frontend-guardian-core 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/bin/fg-core.js +1238 -0
  3. package/bin/watch-mode.js +123 -0
  4. package/dist/engine/cache.d.ts +68 -0
  5. package/dist/engine/cache.d.ts.map +1 -0
  6. package/dist/engine/cache.js +164 -0
  7. package/dist/engine/cache.js.map +1 -0
  8. package/dist/engine/rule-engine.d.ts +135 -0
  9. package/dist/engine/rule-engine.d.ts.map +1 -0
  10. package/dist/engine/rule-engine.js +716 -0
  11. package/dist/engine/rule-engine.js.map +1 -0
  12. package/dist/formatters/github-annotation.d.ts +36 -0
  13. package/dist/formatters/github-annotation.d.ts.map +1 -0
  14. package/dist/formatters/github-annotation.js +122 -0
  15. package/dist/formatters/github-annotation.js.map +1 -0
  16. package/dist/formatters/pr-comment.d.ts +43 -0
  17. package/dist/formatters/pr-comment.d.ts.map +1 -0
  18. package/dist/formatters/pr-comment.js +171 -0
  19. package/dist/formatters/pr-comment.js.map +1 -0
  20. package/dist/formatters/sarif.d.ts +104 -0
  21. package/dist/formatters/sarif.d.ts.map +1 -0
  22. package/dist/formatters/sarif.js +130 -0
  23. package/dist/formatters/sarif.js.map +1 -0
  24. package/dist/index.d.ts +46 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +108 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/integrations/base.d.ts +44 -0
  29. package/dist/integrations/base.d.ts.map +1 -0
  30. package/dist/integrations/base.js +104 -0
  31. package/dist/integrations/base.js.map +1 -0
  32. package/dist/integrations/eslint.d.ts +8 -0
  33. package/dist/integrations/eslint.d.ts.map +1 -0
  34. package/dist/integrations/eslint.js +67 -0
  35. package/dist/integrations/eslint.js.map +1 -0
  36. package/dist/integrations/formatter.d.ts +35 -0
  37. package/dist/integrations/formatter.d.ts.map +1 -0
  38. package/dist/integrations/formatter.js +182 -0
  39. package/dist/integrations/formatter.js.map +1 -0
  40. package/dist/integrations/index.d.ts +17 -0
  41. package/dist/integrations/index.d.ts.map +1 -0
  42. package/dist/integrations/index.js +25 -0
  43. package/dist/integrations/index.js.map +1 -0
  44. package/dist/integrations/stylelint.d.ts +8 -0
  45. package/dist/integrations/stylelint.d.ts.map +1 -0
  46. package/dist/integrations/stylelint.js +59 -0
  47. package/dist/integrations/stylelint.js.map +1 -0
  48. package/dist/integrations/typescript.d.ts +8 -0
  49. package/dist/integrations/typescript.d.ts.map +1 -0
  50. package/dist/integrations/typescript.js +92 -0
  51. package/dist/integrations/typescript.js.map +1 -0
  52. package/dist/rules/registry.d.ts +83 -0
  53. package/dist/rules/registry.d.ts.map +1 -0
  54. package/dist/rules/registry.js +205 -0
  55. package/dist/rules/registry.js.map +1 -0
  56. package/dist/scanners/a11y-scanner.d.ts +14 -0
  57. package/dist/scanners/a11y-scanner.d.ts.map +1 -0
  58. package/dist/scanners/a11y-scanner.js +781 -0
  59. package/dist/scanners/a11y-scanner.js.map +1 -0
  60. package/dist/scanners/component-scanner.d.ts +12 -0
  61. package/dist/scanners/component-scanner.d.ts.map +1 -0
  62. package/dist/scanners/component-scanner.js +304 -0
  63. package/dist/scanners/component-scanner.js.map +1 -0
  64. package/dist/scanners/cross-file-scanner.d.ts +18 -0
  65. package/dist/scanners/cross-file-scanner.d.ts.map +1 -0
  66. package/dist/scanners/cross-file-scanner.js +684 -0
  67. package/dist/scanners/cross-file-scanner.js.map +1 -0
  68. package/dist/scanners/hooks-scanner.d.ts +15 -0
  69. package/dist/scanners/hooks-scanner.d.ts.map +1 -0
  70. package/dist/scanners/hooks-scanner.js +670 -0
  71. package/dist/scanners/hooks-scanner.js.map +1 -0
  72. package/dist/scanners/i18n-scanner.d.ts +13 -0
  73. package/dist/scanners/i18n-scanner.d.ts.map +1 -0
  74. package/dist/scanners/i18n-scanner.js +535 -0
  75. package/dist/scanners/i18n-scanner.js.map +1 -0
  76. package/dist/scanners/naming-scanner.d.ts +19 -0
  77. package/dist/scanners/naming-scanner.d.ts.map +1 -0
  78. package/dist/scanners/naming-scanner.js +746 -0
  79. package/dist/scanners/naming-scanner.js.map +1 -0
  80. package/dist/scanners/performance-scanner.d.ts +7 -0
  81. package/dist/scanners/performance-scanner.d.ts.map +1 -0
  82. package/dist/scanners/performance-scanner.js +402 -0
  83. package/dist/scanners/performance-scanner.js.map +1 -0
  84. package/dist/scanners/platform-scanner.d.ts +15 -0
  85. package/dist/scanners/platform-scanner.d.ts.map +1 -0
  86. package/dist/scanners/platform-scanner.js +320 -0
  87. package/dist/scanners/platform-scanner.js.map +1 -0
  88. package/dist/scanners/security-scanner.d.ts +7 -0
  89. package/dist/scanners/security-scanner.d.ts.map +1 -0
  90. package/dist/scanners/security-scanner.js +349 -0
  91. package/dist/scanners/security-scanner.js.map +1 -0
  92. package/dist/scanners/svelte-scanner.d.ts +14 -0
  93. package/dist/scanners/svelte-scanner.d.ts.map +1 -0
  94. package/dist/scanners/svelte-scanner.js +228 -0
  95. package/dist/scanners/svelte-scanner.js.map +1 -0
  96. package/dist/types.d.ts +343 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +6 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/utils/ast-parser.d.ts +21 -0
  101. package/dist/utils/ast-parser.d.ts.map +1 -0
  102. package/dist/utils/ast-parser.js +119 -0
  103. package/dist/utils/ast-parser.js.map +1 -0
  104. package/dist/utils/baseline.d.ts +89 -0
  105. package/dist/utils/baseline.d.ts.map +1 -0
  106. package/dist/utils/baseline.js +156 -0
  107. package/dist/utils/baseline.js.map +1 -0
  108. package/dist/utils/ci-generator.d.ts +34 -0
  109. package/dist/utils/ci-generator.d.ts.map +1 -0
  110. package/dist/utils/ci-generator.js +194 -0
  111. package/dist/utils/ci-generator.js.map +1 -0
  112. package/dist/utils/common.d.ts +8 -0
  113. package/dist/utils/common.d.ts.map +1 -0
  114. package/dist/utils/common.js +38 -0
  115. package/dist/utils/common.js.map +1 -0
  116. package/dist/utils/concurrent.d.ts +16 -0
  117. package/dist/utils/concurrent.d.ts.map +1 -0
  118. package/dist/utils/concurrent.js +49 -0
  119. package/dist/utils/concurrent.js.map +1 -0
  120. package/dist/utils/config-loader.d.ts +8 -0
  121. package/dist/utils/config-loader.d.ts.map +1 -0
  122. package/dist/utils/config-loader.js +154 -0
  123. package/dist/utils/config-loader.js.map +1 -0
  124. package/dist/utils/fix-bot.d.ts +36 -0
  125. package/dist/utils/fix-bot.d.ts.map +1 -0
  126. package/dist/utils/fix-bot.js +274 -0
  127. package/dist/utils/fix-bot.js.map +1 -0
  128. package/dist/utils/git-hooks.d.ts +55 -0
  129. package/dist/utils/git-hooks.d.ts.map +1 -0
  130. package/dist/utils/git-hooks.js +318 -0
  131. package/dist/utils/git-hooks.js.map +1 -0
  132. package/dist/utils/history-report.d.ts +72 -0
  133. package/dist/utils/history-report.d.ts.map +1 -0
  134. package/dist/utils/history-report.js +144 -0
  135. package/dist/utils/history-report.js.map +1 -0
  136. package/dist/utils/init-config.d.ts +23 -0
  137. package/dist/utils/init-config.d.ts.map +1 -0
  138. package/dist/utils/init-config.js +146 -0
  139. package/dist/utils/init-config.js.map +1 -0
  140. package/dist/utils/pr-publisher.d.ts +64 -0
  141. package/dist/utils/pr-publisher.d.ts.map +1 -0
  142. package/dist/utils/pr-publisher.js +265 -0
  143. package/dist/utils/pr-publisher.js.map +1 -0
  144. package/dist/utils/project-detector.d.ts +20 -0
  145. package/dist/utils/project-detector.d.ts.map +1 -0
  146. package/dist/utils/project-detector.js +342 -0
  147. package/dist/utils/project-detector.js.map +1 -0
  148. package/dist/utils/report-uploader.d.ts +35 -0
  149. package/dist/utils/report-uploader.d.ts.map +1 -0
  150. package/dist/utils/report-uploader.js +106 -0
  151. package/dist/utils/report-uploader.js.map +1 -0
  152. package/package.json +78 -0
package/bin/fg-core.js ADDED
@@ -0,0 +1,1238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Frontend Guardian Core CLI
4
+ * Usage: fg-core [options] <project-dir>
5
+ */
6
+
7
+ import { writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import {
10
+ createEngine,
11
+ i18nRules,
12
+ performanceRules,
13
+ a11yRules,
14
+ securityRules,
15
+ namingRules,
16
+ crossFileRules,
17
+ componentRules,
18
+ hooksRules,
19
+ platformRules,
20
+ svelteRules,
21
+ installGitHooks,
22
+ uninstallGitHooks,
23
+ hasGitHook,
24
+ generateCIConfig,
25
+ detectCIProvider,
26
+ generateSarif,
27
+ formatAllAnnotations,
28
+ isGitHubActions,
29
+ writeJobSummary,
30
+ BaselineManager,
31
+ initConfig,
32
+ detectProjectMeta,
33
+ generatePRComment,
34
+ detectPublisherConfig,
35
+ createPublisher,
36
+ uploadReport,
37
+ detectUploadConfig,
38
+ SmartCache,
39
+ runFixBot,
40
+ detectFixBotConfig,
41
+ } from "../dist/index.js";
42
+ import pc from "picocolors";
43
+ import { runWatchMode } from "./watch-mode.js";
44
+
45
+ const MODULES = [
46
+ "i18n",
47
+ "performance",
48
+ "a11y",
49
+ "security",
50
+ "naming",
51
+ "cross-file",
52
+ "component",
53
+ "hooks",
54
+ "platform",
55
+ "svelte",
56
+ ];
57
+
58
+ const MODULE_RULES = {
59
+ i18n: i18nRules,
60
+ performance: performanceRules,
61
+ a11y: a11yRules,
62
+ security: securityRules,
63
+ naming: namingRules,
64
+ "cross-file": crossFileRules,
65
+ component: componentRules,
66
+ hooks: hooksRules,
67
+ platform: platformRules,
68
+ svelte: svelteRules,
69
+ };
70
+
71
+ function showHelp() {
72
+ console.log(`
73
+ Frontend Guardian Core v2.6.0
74
+
75
+ Usage:
76
+ fg-core <project-dir> [options]
77
+
78
+ Options:
79
+ --module <name> ๆ‰ซๆๆจกๅ—: i18n | performance | a11y | security | naming | cross-file | component | hooks | platform | svelte | all
80
+ --severity <level> ๆœ€ไฝŽไธฅ้‡็บงๅˆซ: critical | warning | suggestion (้ป˜่ฎค: suggestion)
81
+ --files <pattern> ไป…ๆ‰ซๆๅŒน้…็š„ๆ–‡ไปถ
82
+ --exclude <pattern> ๆŽ’้™คๅŒน้…็š„ๆ–‡ไปถ
83
+ --staged ไป…ๆ‰ซๆ git staged ๆ–‡ไปถ
84
+ --diff <range> ไป…ๆ‰ซๆ git diff ่Œƒๅ›ดๅ†…็š„ๆ–‡ไปถ (ๅฆ‚ main...feature)
85
+ --auto-scope ๆ™บ่ƒฝๆ‰ซๆ่Œƒๅ›ด๏ผš่‡ชๅŠจๆฃ€ๆต‹ๆœชๆไบค/ๆœ€่ฟ‘ไฟฎๆ”น็š„ๆ–‡ไปถ
86
+ --no-cluster ็ฆ็”จ Issue ่š็ฑป
87
+ --json ไปฅ JSON ๆ ผๅผ่พ“ๅ‡บ
88
+ --fix ่‡ชๅŠจไฟฎๅคๅฏไฟฎๅค็š„้—ฎ้ข˜
89
+ --dry-run ไฟฎๅค้ข„่งˆๆจกๅผ๏ผˆๅฑ•็คบ diff ไธๅ†™ๅ…ฅๆ–‡ไปถ๏ผ‰
90
+ --interactive ไบคไบ’ๅผไฟฎๅค๏ผˆ้€ๆก็กฎ่ฎค๏ผŒ็ฑปไผผ git add -p๏ผ‰
91
+ --skip-large-files-threshold <bytes> ๅคงๆ–‡ไปถ่ทณ่ฟ‡้˜ˆๅ€ผ๏ผˆ้ป˜่ฎค 512000 = 500KB๏ผŒ0 ่กจ็คบไธ่ทณ่ฟ‡๏ผ‰
92
+ --format ไฟฎๅคๅŽ่‡ชๅŠจๆ ผๅผๅŒ–ไปฃ็ ๏ผˆBiome/Prettier๏ผ‰
93
+ --output <file> ๅฐ†ๆ‰ซๆๆŠฅๅ‘Šๅ†™ๅ…ฅๆŒ‡ๅฎšๆ–‡ไปถ๏ผˆMarkdown ๆ ผๅผ๏ผ‰
94
+ --external ่ฟ่กŒๅค–้ƒจๅทฅๅ…ท้›†ๆˆ (ESLint / TypeScript / Stylelint)
95
+ --watch Watch ๆจกๅผ๏ผšๆ–‡ไปถๅ˜ๆ›ด่‡ชๅŠจๅขž้‡ๆ‰ซๆ
96
+ --no-cache ็ฆ็”จๆ™บ่ƒฝ็ผ“ๅญ˜
97
+ --config <file> ๆŒ‡ๅฎš้…็ฝฎๆ–‡ไปถ
98
+ --install-hooks ๅฎ‰่ฃ… Git hook๏ผˆ้ป˜่ฎค pre-commit๏ผŒๅฏ็”จ --install-hooks-type ๆŒ‡ๅฎš๏ผ‰
99
+ --install-hooks-type <type> hook ็ฑปๅž‹: pre-commit | pre-push | commit-msg | both | all (้ป˜่ฎค: pre-commit)
100
+ --init-config ็”Ÿๆˆ .frontend-guardian.yml ้…็ฝฎๆ–‡ไปถ
101
+ --init-ci ็”Ÿๆˆ CI ้…็ฝฎๆ–‡ไปถ (่‡ชๅŠจๆฃ€ๆต‹ๅนณๅฐ๏ผŒ้ป˜่ฎค GitHub Actions)
102
+ --init-ci-provider <p> CI ๅนณๅฐ: github | gitlab | both (้ป˜่ฎค: ่‡ชๅŠจๆฃ€ๆต‹)
103
+ --sarif <file> ่พ“ๅ‡บ SARIF ๆ ผๅผๆŠฅๅ‘ŠๅˆฐๆŒ‡ๅฎšๆ–‡ไปถ
104
+ --github-actions ๅฏ็”จ GitHub Actions Annotation ่พ“ๅ‡บ
105
+ --baseline <file> Baseline ๆจกๅผ๏ผšไป…ๆŠฅๅ‘Šๆ–ฐๅขž้—ฎ้ข˜
106
+ --generate-baseline ็”Ÿๆˆ baseline ๆ–‡ไปถ๏ผˆ้œ€ๅŒๆ—ถๆŒ‡ๅฎš --baseline๏ผ‰
107
+ --post-comment ๅฐ†ๆ‰ซๆ็ป“ๆžœๅ‘ๅธƒไธบ PR/MR ่ฏ„่ฎบ๏ผˆ่‡ชๅŠจๆฃ€ๆต‹ CI ็Žฏๅขƒ๏ผ‰
108
+ --pr-number <n> ๆŒ‡ๅฎš PR/MR ็ผ–ๅท๏ผˆ้…ๅˆ --post-comment๏ผ‰
109
+ --comment-provider <p> ่ฏ„่ฎบๅนณๅฐ: github | gitlab๏ผˆ้…ๅˆ --post-comment๏ผ‰
110
+ --upload ไธŠไผ ๆŠฅๅ‘ŠๅˆฐๆŒ‡ๅฎšไฝ็ฝฎ๏ผˆ้œ€้…็ฝฎ FG_UPLOAD_PROVIDER ็ญ‰็Žฏๅขƒๅ˜้‡๏ผ‰
111
+ --help, -h ๆ˜พ็คบๅธฎๅŠฉ
112
+
113
+ Examples:
114
+ fg-core ./my-project --module all
115
+ fg-core ./my-project --module i18n
116
+ fg-core ./my-project --module i18n --severity warning --json
117
+ fg-core ./my-project --module performance --files "src/**/*.tsx"
118
+ fg-core ./my-project --module all --fix --json
119
+ fg-core ./my-project --module all --staged
120
+ fg-core ./my-project --module all --diff main...feature
121
+ fg-core ./my-project --install-hooks
122
+ fg-core ./my-project --init-ci
123
+ `);
124
+ }
125
+
126
+ async function main() {
127
+ const args = process.argv.slice(2);
128
+ const projectDir = args.find((arg) => !arg.startsWith("-")) || process.cwd();
129
+
130
+ const options = {
131
+ projectDir,
132
+ minSeverity: "suggestion",
133
+ module: "i18n",
134
+ json: false,
135
+ fix: false,
136
+ dryRun: false,
137
+ configFile: undefined,
138
+ files: undefined,
139
+ exclude: undefined,
140
+ staged: false,
141
+ diffRange: undefined,
142
+ cluster: true,
143
+ external: false,
144
+ watch: false,
145
+ cache: true,
146
+ format: false,
147
+ interactive: false,
148
+ skipLargeFilesThreshold: undefined,
149
+ installHooks: false,
150
+ installHooksType: "pre-commit",
151
+ initConfig: false,
152
+ initCi: false,
153
+ initCiProvider: undefined,
154
+ autoScope: false,
155
+ sarif: undefined,
156
+ githubActions: false,
157
+ baseline: undefined,
158
+ generateBaseline: false,
159
+ postComment: false,
160
+ prNumber: undefined,
161
+ commentProvider: undefined,
162
+ upload: false,
163
+ output: undefined,
164
+ fixBot: false,
165
+ };
166
+
167
+ for (let i = 0; i < args.length; i++) {
168
+ switch (args[i]) {
169
+ case "--module":
170
+ options.module = args[++i];
171
+ break;
172
+ case "--severity":
173
+ options.minSeverity = args[++i];
174
+ break;
175
+ case "--files":
176
+ options.files = args[++i].split(",");
177
+ break;
178
+ case "--exclude":
179
+ options.exclude = args[++i].split(",");
180
+ break;
181
+ case "--config":
182
+ options.configFile = args[++i];
183
+ break;
184
+ case "--diff":
185
+ options.diffRange = args[++i];
186
+ break;
187
+ case "--auto-scope":
188
+ options.autoScope = true;
189
+ break;
190
+ case "--json":
191
+ options.json = true;
192
+ break;
193
+ case "--fix":
194
+ options.fix = true;
195
+ break;
196
+ case "--staged":
197
+ options.staged = true;
198
+ break;
199
+ case "--no-cluster":
200
+ options.cluster = false;
201
+ break;
202
+ case "--external":
203
+ options.external = true;
204
+ break;
205
+ case "--dry-run":
206
+ options.dryRun = true;
207
+ break;
208
+ case "--watch":
209
+ options.watch = true;
210
+ break;
211
+ case "--no-cache":
212
+ options.cache = false;
213
+ break;
214
+ case "--format":
215
+ options.format = true;
216
+ break;
217
+ case "--output":
218
+ options.output = args[++i];
219
+ break;
220
+ case "--interactive":
221
+ options.interactive = true;
222
+ break;
223
+ case "--skip-large-files-threshold":
224
+ options.skipLargeFilesThreshold = parseInt(args[++i], 10);
225
+ break;
226
+ case "--install-hooks":
227
+ options.installHooks = true;
228
+ break;
229
+ case "--install-hooks-type":
230
+ options.installHooksType = args[++i];
231
+ break;
232
+ case "--init-config":
233
+ options.initConfig = true;
234
+ break;
235
+ case "--init-ci":
236
+ options.initCi = true;
237
+ break;
238
+ case "--init-ci-provider":
239
+ options.initCiProvider = args[++i];
240
+ break;
241
+ case "--sarif":
242
+ options.sarif = args[++i];
243
+ break;
244
+ case "--github-actions":
245
+ options.githubActions = true;
246
+ break;
247
+ case "--baseline":
248
+ options.baseline = args[++i];
249
+ break;
250
+ case "--generate-baseline":
251
+ options.generateBaseline = true;
252
+ break;
253
+ case "--post-comment":
254
+ options.postComment = true;
255
+ break;
256
+ case "--pr-number":
257
+ options.prNumber = parseInt(args[++i], 10);
258
+ break;
259
+ case "--comment-provider":
260
+ options.commentProvider = args[++i];
261
+ break;
262
+ case "--upload":
263
+ options.upload = true;
264
+ break;
265
+ case "--fix-bot":
266
+ options.fixBot = true;
267
+ break;
268
+ case "--help":
269
+ case "-h":
270
+ showHelp();
271
+ process.exit(0);
272
+ }
273
+ }
274
+
275
+ // Phase 6: ็‰นๆฎŠๅ‘ฝไปคๅค„็†
276
+ if (options.installHooks) {
277
+ const hookType = ["pre-commit", "pre-push", "commit-msg", "both", "all"].includes(options.installHooksType)
278
+ ? options.installHooksType
279
+ : "pre-commit";
280
+ const result = installGitHooks(options.projectDir, {
281
+ type: hookType,
282
+ autoFix: false,
283
+ cache: true,
284
+ });
285
+ console.log(pc.cyan("๐Ÿ”ง Git Hook ๅฎ‰่ฃ…็ป“ๆžœ"));
286
+ for (const h of result.installed) {
287
+ console.log(pc.green(` โœ… ๅทฒๅฎ‰่ฃ…: ${h}`));
288
+ }
289
+ for (const h of result.skipped) {
290
+ console.log(pc.yellow(` โš ๏ธ ่ทณ่ฟ‡: ${h}`));
291
+ }
292
+ if (result.installed.length === 0 && result.skipped.length === 0) {
293
+ console.log(pc.gray(" ๆœชๆ‰พๅˆฐ Git ไป“ๅบ“"));
294
+ }
295
+ process.exit(0);
296
+ }
297
+
298
+ if (options.initConfig) {
299
+ const meta = detectProjectMeta(options.projectDir);
300
+ const result = initConfig(options.projectDir, meta, false);
301
+ console.log(pc.cyan("๐Ÿ”ง ้…็ฝฎๅˆๅง‹ๅŒ–"));
302
+ if (result.existed) {
303
+ console.log(pc.yellow(` โš ๏ธ ้…็ฝฎๆ–‡ไปถๅทฒๅญ˜ๅœจ: ${result.path}`));
304
+ console.log(pc.gray(" ไฝฟ็”จ --init-config --force ่ฆ†็›–๏ผˆๆˆ–ไฝฟ็”จ --init-config ็›ดๆŽฅ่ฆ†็›–๏ผ‰"));
305
+ } else if (result.created) {
306
+ console.log(pc.green(` โœ… ๅทฒๅˆ›ๅปบ: ${result.path}`));
307
+ console.log(pc.gray(` ๆก†ๆžถ: ${meta.framework ?? "auto-detect"} | ็ป„ไปถๅบ“: ${meta.componentLib ?? "auto-detect"}`));
308
+ }
309
+ process.exit(0);
310
+ }
311
+
312
+ if (options.initCi) {
313
+ let provider = options.initCiProvider;
314
+ if (!provider) {
315
+ provider = detectCIProvider(options.projectDir);
316
+ console.log(pc.gray(` ่‡ชๅŠจๆฃ€ๆต‹ๅˆฐ CI ๅนณๅฐ: ${provider}`));
317
+ }
318
+ const result = generateCIConfig(options.projectDir, {
319
+ provider,
320
+ runTests: true,
321
+ gate: true,
322
+ });
323
+ console.log(pc.cyan("๐Ÿ”ง CI ้…็ฝฎ็”Ÿๆˆ็ป“ๆžœ"));
324
+ for (const f of result.created) {
325
+ console.log(pc.green(` โœ… ๅทฒๅˆ›ๅปบ: ${f}`));
326
+ }
327
+ if (result.created.length === 0) {
328
+ console.log(pc.gray(" ๆœช็”Ÿๆˆ้…็ฝฎๆ–‡ไปถ"));
329
+ }
330
+ process.exit(0);
331
+ }
332
+
333
+ if (options.watch) {
334
+ const scanFn = options.module === "all" ? runAllModules : runSingleModule;
335
+ // v2.6.0: Watch ๆจกๅผๅค็”จ SmartCache๏ผŒๅฎž็Žฐ็ผ“ๅญ˜้ข„็ƒญ
336
+ const cacheInstance = options.cache !== false ? new SmartCache(options.projectDir) : undefined;
337
+ await runWatchMode(options, scanFn, cacheInstance);
338
+ } else if (options.module === "all") {
339
+ await runAllModules(options);
340
+ } else {
341
+ await runSingleModule(options);
342
+ }
343
+ }
344
+
345
+ async function runAllModules(options, cacheInstance) {
346
+ console.log(pc.cyan("๐Ÿ›ก๏ธ Frontend Guardian Core"));
347
+ console.log(pc.gray(` Project: ${options.projectDir}`));
348
+ console.log(pc.gray(` Module: all (9 modules)`));
349
+ if (options.staged) {
350
+ console.log(pc.gray(` Mode: staged (git cached only)`));
351
+ } else if (options.diffRange) {
352
+ console.log(pc.gray(` Diff: ${options.diffRange}`));
353
+ } else if (options.autoScope) {
354
+ console.log(pc.gray(` Mode: auto-scope (ๆ™บ่ƒฝๆ‰ซๆ่Œƒๅ›ด)`));
355
+ }
356
+ console.log("");
357
+
358
+ const engine = createEngine({
359
+ projectDir: options.projectDir,
360
+ minSeverity: options.minSeverity,
361
+ files: options.files,
362
+ exclude: options.exclude,
363
+ configFile: options.configFile,
364
+ staged: options.staged,
365
+ diffRange: options.diffRange,
366
+ autoScope: options.autoScope,
367
+ external: options.external,
368
+ cache: options.cache,
369
+ cacheInstance,
370
+ dryRun: options.dryRun,
371
+ interactive: options.interactive,
372
+ skipLargeFilesThreshold: options.skipLargeFilesThreshold,
373
+ });
374
+
375
+ // ๆณจๅ†Œๆ‰€ๆœ‰ๆจกๅ—็š„่ง„ๅˆ™
376
+ for (const rules of Object.values(MODULE_RULES)) {
377
+ engine.registerAll(rules);
378
+ }
379
+
380
+ const allResults = {};
381
+ let totalCritical = 0;
382
+ let totalWarning = 0;
383
+ let totalSuggestion = 0;
384
+ let totalDuration = 0;
385
+ let totalFilesScanned = 0;
386
+ let totalFilesWithIssues = 0;
387
+ const allFixableIssues = [];
388
+
389
+ // Phase 4: ๅค–้ƒจๅทฅๅ…ท้›†ๆˆ
390
+ let externalResults = [];
391
+ if (options.external) {
392
+ externalResults = engine.runExternal();
393
+ for (const er of externalResults) {
394
+ for (const issue of er.issues) {
395
+ totalCritical += issue.severity === "critical" ? 1 : 0;
396
+ totalWarning += issue.severity === "warning" ? 1 : 0;
397
+ totalSuggestion += issue.severity === "suggestion" ? 1 : 0;
398
+ totalFilesWithIssues += 1;
399
+ }
400
+ totalDuration += er.duration;
401
+ }
402
+ }
403
+
404
+ for (const mod of MODULES) {
405
+ try {
406
+ let result = await engine.scan(mod);
407
+
408
+ // Issue ่š็ฑป
409
+ if (options.cluster) {
410
+ result = {
411
+ ...result,
412
+ issues: {
413
+ critical: engine.clusterIssues(result.issues.critical),
414
+ warning: engine.clusterIssues(result.issues.warning),
415
+ suggestion: engine.clusterIssues(result.issues.suggestion),
416
+ },
417
+ total:
418
+ engine.clusterIssues(result.issues.critical).length +
419
+ engine.clusterIssues(result.issues.warning).length +
420
+ engine.clusterIssues(result.issues.suggestion).length,
421
+ };
422
+ }
423
+
424
+ allResults[mod] = result;
425
+ totalCritical += result.issues.critical.length;
426
+ totalWarning += result.issues.warning.length;
427
+ totalSuggestion += result.issues.suggestion.length;
428
+ totalDuration += result.duration;
429
+ totalFilesScanned = Math.max(totalFilesScanned, result.filesScanned);
430
+ if (result.filesWithIssues > 0) {
431
+ totalFilesWithIssues += result.filesWithIssues;
432
+ }
433
+
434
+ if (options.fix) {
435
+ const moduleIssues = [...result.issues.critical, ...result.issues.warning, ...result.issues.suggestion];
436
+ allFixableIssues.push(...moduleIssues.filter((i) => i.fix));
437
+ }
438
+ } catch (err) {
439
+ console.error(pc.red(` โŒ [${mod}] ๆ‰ซๆๅคฑ่ดฅ:`), err.message || err);
440
+ }
441
+ }
442
+
443
+ // ่‡ชๅŠจไฟฎๅค / ไฟฎๅค้ข„่งˆ
444
+ let fixResult = null;
445
+ if ((options.fix || options.dryRun) && allFixableIssues.length > 0) {
446
+ if (!options.json) {
447
+ if (options.dryRun) {
448
+ console.log(pc.cyan("๐Ÿ” ไฟฎๅค้ข„่งˆ๏ผˆdry-run ๆจกๅผ๏ผŒไธไผšไฟฎๆ”นๆ–‡ไปถ๏ผ‰๏ผš"));
449
+ } else {
450
+ console.log(pc.cyan("๐Ÿ”ง ๆญฃๅœจๅบ”็”จ่‡ชๅŠจไฟฎๅค..."));
451
+ }
452
+ }
453
+ fixResult = engine.applyFixes(allFixableIssues);
454
+ if (!options.json) {
455
+ if (options.dryRun && fixResult.previews) {
456
+ for (const preview of fixResult.previews) {
457
+ console.log(pc.cyan(`\n ๐Ÿ“„ ${preview.file}`));
458
+ console.log(pc.yellow(` ${preview.title}`));
459
+ console.log(preview.diff);
460
+ }
461
+ console.log(pc.blue(`\n ๐Ÿ’ก ๅ…ฑ ${fixResult.previews.length} ๅค„ๅฏไฟฎๅค๏ผˆไฝฟ็”จ --fix ๅบ”็”จ๏ผ‰`));
462
+ } else if (fixResult.filesModified.length > 0) {
463
+ console.log(
464
+ pc.green(` โœ… ๅทฒไฟฎๅค ${fixResult.fixedCount} ไธช้—ฎ้ข˜๏ผˆ${fixResult.filesModified.length} ไธชๆ–‡ไปถ๏ผ‰`)
465
+ );
466
+ for (const f of fixResult.filesModified) {
467
+ console.log(pc.gray(` - ${f}`));
468
+ }
469
+ if (fixResult.skippedByUser && fixResult.skippedByUser > 0) {
470
+ console.log(pc.yellow(` โญ๏ธ ่ทณ่ฟ‡ ${fixResult.skippedByUser} ไธช้—ฎ้ข˜๏ผˆไฝŽ็ฝฎไฟกๅบฆๆˆ–็”จๆˆท้€‰ๆ‹ฉ๏ผ‰`));
471
+ }
472
+ } else {
473
+ console.log(pc.yellow(" โš ๏ธ ๆœชๅบ”็”จไปปไฝ•ไฟฎๅค"));
474
+ }
475
+ if (fixResult.errors.length > 0) {
476
+ for (const err of fixResult.errors) {
477
+ console.log(pc.red(` โŒ ${err}`));
478
+ }
479
+ }
480
+ console.log("");
481
+ }
482
+ }
483
+
484
+ // ๆ ผๅผๅŒ–๏ผˆ--format๏ผ‰
485
+ let formatResult = null;
486
+ if (options.format && !options.dryRun) {
487
+ const filesToFormat = options.fix && fixResult?.filesModified ? fixResult.filesModified : undefined;
488
+ if (!options.json) {
489
+ console.log(pc.cyan("๐ŸŽจ ๆญฃๅœจๆ ผๅผๅŒ–ไปฃ็ ..."));
490
+ }
491
+ formatResult = engine.format(filesToFormat);
492
+ if (!options.json) {
493
+ if (formatResult.formatted > 0) {
494
+ console.log(pc.green(` โœ… ${formatResult.formatter} ๅทฒๆ ผๅผๅŒ– ${formatResult.formatted} ไธชๆ–‡ไปถ`));
495
+ } else if (formatResult.errors.length === 0) {
496
+ console.log(pc.gray(" ๆ‰€ๆœ‰ๆ–‡ไปถๅทฒๆ˜ฏๆœ€ไผ˜ๆ ผๅผ"));
497
+ }
498
+ if (formatResult.errors.length > 0) {
499
+ for (const err of formatResult.errors) {
500
+ console.log(pc.red(` โŒ ${err}`));
501
+ }
502
+ }
503
+ console.log("");
504
+ }
505
+ }
506
+
507
+ // v2.6.0: ่‡ชๅŠจไฟฎๅค Bot
508
+ if (options.fixBot && fixResult?.filesModified?.length > 0) {
509
+ const botConfig = detectFixBotConfig();
510
+ if (botConfig) {
511
+ if (!options.json) {
512
+ console.log(pc.cyan("๐Ÿค– ๆญฃๅœจๅˆ›ๅปบไฟฎๅค PR..."));
513
+ }
514
+ const botResult = await runFixBot(options.projectDir, fixResult.filesModified, botConfig);
515
+ if (!options.json) {
516
+ if (botResult.success) {
517
+ console.log(pc.green(` โœ… ไฟฎๅค PR ๅทฒๅˆ›ๅปบ`));
518
+ console.log(pc.gray(` ๅˆ†ๆ”ฏ: ${botResult.branch}`));
519
+ if (botResult.prUrl) {
520
+ console.log(pc.gray(` ${botResult.prUrl}`));
521
+ }
522
+ } else {
523
+ console.log(pc.red(` โŒ Fix Bot ๅคฑ่ดฅ: ${botResult.error}`));
524
+ }
525
+ console.log("");
526
+ }
527
+ } else {
528
+ if (!options.json) {
529
+ console.log(
530
+ pc.yellow(
531
+ " โš ๏ธ ๆœชๆฃ€ๆต‹ๅˆฐ Fix Bot ้…็ฝฎใ€‚่ฏท่ฎพ็ฝฎ FG_FIX_BOT_PROVIDER ๅ’Œ FG_FIX_BOT_TOKEN ็Žฏๅขƒๅ˜้‡ใ€‚"
532
+ )
533
+ );
534
+ console.log("");
535
+ }
536
+ }
537
+ }
538
+
539
+ // v2.3.0: Baseline ๆจกๅผ โ€”โ€” ็”Ÿๆˆๆˆ–ๅบ”็”จ baseline ่ฟ‡ๆปค
540
+ let baselineResult = null;
541
+ if (options.baseline) {
542
+ const allIssues = collectAllIssues(allResults, externalResults);
543
+ if (options.generateBaseline) {
544
+ const baseline = new BaselineManager(options.baseline, options.projectDir);
545
+ baseline.save(allIssues, { projectDir: options.projectDir });
546
+ if (!options.json) {
547
+ console.log(pc.cyan(`๐Ÿ“‹ Baseline ๅทฒ็”Ÿๆˆ: ${baseline.getPath()}`));
548
+ console.log(pc.gray(` ๅŒ…ๅซ ${allIssues.length} ไธชๅทฒ็Ÿฅ้—ฎ้ข˜`));
549
+ }
550
+ process.exit(0);
551
+ }
552
+ baselineResult = applyBaselineToResults(allResults, externalResults, options.baseline, options.projectDir);
553
+ if (baselineResult.baselineLoaded && !options.json) {
554
+ console.log(
555
+ pc.cyan(
556
+ `๐Ÿ“‹ Baseline ๆจกๅผ: ๅฟฝ็•ฅ ${baselineResult.knownIssues.length} ไธชๅทฒ็Ÿฅ้—ฎ้ข˜๏ผŒๅ…ณๆณจ ${baselineResult.newIssues.length} ไธชๆ–ฐๅขž้—ฎ้ข˜`
557
+ )
558
+ );
559
+ console.log("");
560
+ }
561
+ const newTotals = recalculateTotals(allResults, externalResults);
562
+ totalCritical = newTotals.critical;
563
+ totalWarning = newTotals.warning;
564
+ totalSuggestion = newTotals.suggestion;
565
+ }
566
+
567
+ // v2.3.0: SARIF ่พ“ๅ‡บ
568
+ if (options.sarif) {
569
+ const allIssues = collectAllIssues(allResults, externalResults);
570
+ const sarif = generateSarif(allIssues, {
571
+ toolName: "Frontend Guardian",
572
+ toolVersion: "2.3.0",
573
+ projectDir: options.projectDir,
574
+ });
575
+ writeFileSync(options.sarif, JSON.stringify(sarif, null, 2), "utf-8");
576
+ if (!options.json && !options.githubActions) {
577
+ console.log(pc.cyan(`๐Ÿ“„ SARIF ๆŠฅๅ‘Šๅทฒไฟๅญ˜: ${options.sarif}`));
578
+ console.log(pc.gray(` ๅŒ…ๅซ ${allIssues.length} ไธช้—ฎ้ข˜`));
579
+ console.log("");
580
+ }
581
+ }
582
+
583
+ // v2.3.0: GitHub Actions Annotation
584
+ if (options.githubActions || isGitHubActions()) {
585
+ const allIssues = collectAllIssues(allResults, externalResults);
586
+ if (allIssues.length > 0) {
587
+ const annotations = formatAllAnnotations(allIssues);
588
+ console.log(annotations);
589
+ }
590
+ writeJobSummary(allIssues, { totalFilesScanned, duration: totalDuration });
591
+ }
592
+
593
+ // v2.5.0: PR/MR ่ฏ„่ฎบ่‡ชๅŠจๅ‘ๅธƒ
594
+ if (options.postComment) {
595
+ const commentBody = generatePRComment(
596
+ allResults,
597
+ {
598
+ timestamp: new Date().toISOString(),
599
+ commitSha: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA,
600
+ duration: totalDuration,
601
+ filesScanned: totalFilesScanned,
602
+ },
603
+ {
604
+ external: externalResults.length > 0 ? externalResults : undefined,
605
+ fixResult: fixResult
606
+ ? { fixedCount: fixResult.fixedCount, filesModified: fixResult.filesModified }
607
+ : null,
608
+ }
609
+ );
610
+
611
+ // ๆž„ๅปบๅ‘ๅธƒๅ™จ้…็ฝฎ
612
+ let pubConfig = detectPublisherConfig();
613
+ if (!pubConfig && options.commentProvider) {
614
+ const token =
615
+ options.commentProvider === "github"
616
+ ? process.env.GITHUB_TOKEN
617
+ : process.env.GITLAB_TOKEN;
618
+ const repo =
619
+ options.commentProvider === "github"
620
+ ? process.env.GITHUB_REPOSITORY
621
+ : process.env.CI_PROJECT_ID;
622
+ if (token && repo && options.prNumber) {
623
+ pubConfig = {
624
+ provider: options.commentProvider,
625
+ token,
626
+ repository: repo,
627
+ prNumber: options.prNumber,
628
+ };
629
+ }
630
+ }
631
+
632
+ if (pubConfig) {
633
+ try {
634
+ const publisher = createPublisher(pubConfig);
635
+ const result = await publisher.publish(commentBody);
636
+ if (result.success) {
637
+ console.log(
638
+ pc.cyan(`๐Ÿ’ฌ PR/MR ่ฏ„่ฎบๅทฒ${result.action === "updated" ? "ๆ›ดๆ–ฐ" : "ๅ‘ๅธƒ"}`)
639
+ );
640
+ if (result.commentUrl) {
641
+ console.log(pc.gray(` ${result.commentUrl}`));
642
+ }
643
+ } else {
644
+ console.log(pc.red(` โŒ ่ฏ„่ฎบๅ‘ๅธƒๅคฑ่ดฅ: ${result.error}`));
645
+ }
646
+ } catch (err) {
647
+ console.log(pc.red(` โŒ ่ฏ„่ฎบๅ‘ๅธƒๅผ‚ๅธธ: ${err.message || err}`));
648
+ }
649
+ } else {
650
+ console.log(
651
+ pc.yellow(
652
+ " โš ๏ธ ๆ— ๆณ•ๆฃ€ๆต‹ CI ็Žฏๅขƒ๏ผŒ่ทณ่ฟ‡่ฏ„่ฎบๅ‘ๅธƒใ€‚่ฏทๆฃ€ๆŸฅ GITHUB_TOKEN / GITLAB_TOKEN ็Žฏๅขƒๅ˜้‡๏ผŒๆˆ–ๆ˜พๅผๆŒ‡ๅฎš --comment-provider ๅ’Œ --pr-number"
653
+ )
654
+ );
655
+ }
656
+ console.log("");
657
+ }
658
+
659
+ // ็”Ÿๆˆๅนถๅ†™ๅ…ฅๆŠฅๅ‘Šๆ–‡ไปถ๏ผˆ--output๏ผ‰
660
+ let reportPath = options.output;
661
+ if (reportPath) {
662
+ const reportBody = generatePRComment(
663
+ allResults,
664
+ {
665
+ timestamp: new Date().toISOString(),
666
+ commitSha: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA,
667
+ duration: totalDuration,
668
+ filesScanned: totalFilesScanned,
669
+ },
670
+ {
671
+ external: externalResults.length > 0 ? externalResults : undefined,
672
+ fixResult: fixResult
673
+ ? { fixedCount: fixResult.fixedCount, filesModified: fixResult.filesModified }
674
+ : null,
675
+ }
676
+ );
677
+ writeFileSync(reportPath, reportBody, "utf-8");
678
+ if (!options.json) {
679
+ console.log(pc.cyan("๐Ÿ“„ ๆŠฅๅ‘Šๅทฒๅ†™ๅ…ฅ"));
680
+ console.log(pc.gray(` ${reportPath}`));
681
+ console.log("");
682
+ }
683
+ }
684
+
685
+ // v2.5.0: ๆŠฅๅ‘ŠไธŠไผ ๏ผˆ--upload๏ผ‰
686
+ if (options.upload) {
687
+ const uploadConfig = detectUploadConfig();
688
+ if (uploadConfig) {
689
+ const targetPath = reportPath || (() => {
690
+ const tmpPath = join(process.cwd(), `fg-report-${Date.now()}.md`);
691
+ const reportBody = generatePRComment(
692
+ allResults,
693
+ {
694
+ timestamp: new Date().toISOString(),
695
+ duration: totalDuration,
696
+ filesScanned: totalFilesScanned,
697
+ },
698
+ {
699
+ external: externalResults.length > 0 ? externalResults : undefined,
700
+ fixResult: fixResult
701
+ ? { fixedCount: fixResult.fixedCount, filesModified: fixResult.filesModified }
702
+ : null,
703
+ }
704
+ );
705
+ writeFileSync(tmpPath, reportBody, "utf-8");
706
+ return tmpPath;
707
+ })();
708
+ const uploadResult = await uploadReport(targetPath, uploadConfig);
709
+ if (uploadResult.success) {
710
+ console.log(pc.cyan("๐Ÿ“ค ๆŠฅๅ‘ŠๅทฒไธŠไผ "));
711
+ if (uploadResult.reportUrl) {
712
+ console.log(pc.gray(` ${uploadResult.reportUrl}`));
713
+ }
714
+ } else {
715
+ console.log(pc.red(` โŒ ไธŠไผ ๅคฑ่ดฅ: ${uploadResult.error}`));
716
+ }
717
+ console.log("");
718
+ } else {
719
+ console.log(
720
+ pc.yellow(
721
+ " โš ๏ธ ๆœชๆฃ€ๆต‹ๅˆฐไธŠไผ ้…็ฝฎใ€‚่ฏท่ฎพ็ฝฎ FG_UPLOAD_PROVIDER ๅ’Œ FG_UPLOAD_URL / FG_UPLOAD_DIR ็Žฏๅขƒๅ˜้‡ใ€‚"
722
+ )
723
+ );
724
+ console.log("");
725
+ }
726
+ }
727
+
728
+ if (options.json) {
729
+ const jsonOutput = {
730
+ modules: allResults,
731
+ fix: fixResult,
732
+ format: formatResult,
733
+ external: externalResults.length > 0 ? externalResults : undefined,
734
+ summary: {
735
+ totalFilesScanned,
736
+ issuesBySeverity: {
737
+ critical: totalCritical,
738
+ warning: totalWarning,
739
+ suggestion: totalSuggestion,
740
+ },
741
+ totalDuration,
742
+ },
743
+ };
744
+ console.log(JSON.stringify(jsonOutput, null, 2));
745
+ process.exit(totalCritical > 0 ? 1 : 0);
746
+ }
747
+
748
+ // Phase 4: ็ปˆ็ซฏ่พ“ๅ‡บๅค–้ƒจๅทฅๅ…ท็ป“ๆžœ
749
+ if (externalResults.length > 0) {
750
+ console.log(pc.cyan("๐Ÿ”Œ ๅค–้ƒจๅทฅๅ…ทๆฃ€ๆŸฅ็ป“ๆžœ"));
751
+ for (const er of externalResults) {
752
+ if (er.issues.length === 0) continue;
753
+ const color = er.issues.some((i) => i.severity === "critical")
754
+ ? pc.red
755
+ : er.issues.some((i) => i.severity === "warning")
756
+ ? pc.yellow
757
+ : pc.blue;
758
+ console.log(color(` ${er.tool}: ${er.issues.length} ไธช้—ฎ้ข˜`));
759
+ }
760
+ console.log("");
761
+ }
762
+
763
+ // ็ปˆ็ซฏ่พ“ๅ‡บ
764
+ if (totalCritical + totalWarning + totalSuggestion === 0) {
765
+ console.log(pc.green("โœ… ๆœชๅ‘็Žฐ้—ฎ้ข˜"));
766
+ } else {
767
+ console.log("");
768
+ console.log(pc.cyan("๐Ÿ“Š ๆ‰ซๆ็ป“ๆžœๆฑ‡ๆ€ป"));
769
+ console.log(pc.red(` ๐Ÿ”ด Critical: ${totalCritical}`));
770
+ console.log(pc.yellow(` ๐ŸŸก Warning: ${totalWarning}`));
771
+ console.log(pc.blue(` ๐Ÿ’ก Suggestion: ${totalSuggestion}`));
772
+ console.log("");
773
+
774
+ for (const mod of MODULES) {
775
+ const r = allResults[mod];
776
+ if (!r || r.total === 0) continue;
777
+
778
+ console.log(pc.cyan(`๐Ÿ“ฆ ${mod}`));
779
+ if (r.issues.critical.length) {
780
+ console.log(pc.red(` ๐Ÿ”ด Critical: ${r.issues.critical.length}`));
781
+ for (const issue of r.issues.critical) {
782
+ printIssue(issue, pc.red);
783
+ }
784
+ }
785
+ if (r.issues.warning.length) {
786
+ console.log(pc.yellow(` ๐ŸŸก Warning: ${r.issues.warning.length}`));
787
+ for (const issue of r.issues.warning) {
788
+ printIssue(issue, pc.yellow);
789
+ }
790
+ }
791
+ if (r.issues.suggestion.length) {
792
+ console.log(pc.blue(` ๐Ÿ’ก Suggestion: ${r.issues.suggestion.length}`));
793
+ for (const issue of r.issues.suggestion) {
794
+ printIssue(issue, pc.blue);
795
+ }
796
+ }
797
+ console.log("");
798
+ }
799
+ }
800
+
801
+ console.log(
802
+ pc.gray(
803
+ `โฑ๏ธ ๆ€ป่€—ๆ—ถ: ${totalDuration}ms | ๆ‰ซๆ ${totalFilesScanned} ไธชๆ–‡ไปถ | ${totalFilesWithIssues} ไธชๆ–‡ไปถๆœ‰้—ฎ้ข˜`
804
+ )
805
+ );
806
+
807
+ if (totalCritical > 0) {
808
+ process.exit(1);
809
+ }
810
+ }
811
+
812
+ async function runSingleModule(options, cacheInstance) {
813
+ console.log(pc.cyan("๐Ÿ›ก๏ธ Frontend Guardian Core"));
814
+ console.log(pc.gray(` Project: ${options.projectDir}`));
815
+ console.log(pc.gray(` Module: ${options.module}`));
816
+ if (options.staged) {
817
+ console.log(pc.gray(` Mode: staged (git cached only)`));
818
+ } else if (options.diffRange) {
819
+ console.log(pc.gray(` Diff: ${options.diffRange}`));
820
+ } else if (options.autoScope) {
821
+ console.log(pc.gray(` Mode: auto-scope (ๆ™บ่ƒฝๆ‰ซๆ่Œƒๅ›ด)`));
822
+ }
823
+ console.log("");
824
+
825
+ const engine = createEngine({
826
+ projectDir: options.projectDir,
827
+ minSeverity: options.minSeverity,
828
+ files: options.files,
829
+ exclude: options.exclude,
830
+ configFile: options.configFile,
831
+ staged: options.staged,
832
+ diffRange: options.diffRange,
833
+ autoScope: options.autoScope,
834
+ cache: options.cache,
835
+ cacheInstance,
836
+ dryRun: options.dryRun,
837
+ interactive: options.interactive,
838
+ skipLargeFilesThreshold: options.skipLargeFilesThreshold,
839
+ });
840
+
841
+ // ๆณจๅ†Œ่ง„ๅˆ™
842
+ if (MODULE_RULES[options.module]) {
843
+ engine.registerAll(MODULE_RULES[options.module]);
844
+ } else {
845
+ console.log(pc.red(`โŒ ๆœช็Ÿฅๆจกๅ—: ${options.module}`));
846
+ console.log(pc.gray(` ๅฏ็”จๆจกๅ—: ${MODULES.join(", ")}, all`));
847
+ process.exit(1);
848
+ }
849
+
850
+ try {
851
+ let result = await engine.scan(options.module);
852
+
853
+ // Issue ่š็ฑป
854
+ if (options.cluster) {
855
+ result = {
856
+ ...result,
857
+ issues: {
858
+ critical: engine.clusterIssues(result.issues.critical),
859
+ warning: engine.clusterIssues(result.issues.warning),
860
+ suggestion: engine.clusterIssues(result.issues.suggestion),
861
+ },
862
+ total:
863
+ engine.clusterIssues(result.issues.critical).length +
864
+ engine.clusterIssues(result.issues.warning).length +
865
+ engine.clusterIssues(result.issues.suggestion).length,
866
+ };
867
+ }
868
+
869
+ const { issues } = result;
870
+ let totalCritical = issues.critical.length;
871
+ let totalWarning = issues.warning.length;
872
+ let totalSuggestion = issues.suggestion.length;
873
+
874
+ // ่‡ชๅŠจไฟฎๅค / ไฟฎๅค้ข„่งˆ
875
+ let fixResult = null;
876
+ if (options.fix || options.dryRun) {
877
+ const allIssues = [...issues.critical, ...issues.warning, ...issues.suggestion];
878
+ const fixableIssues = allIssues.filter((i) => i.fix);
879
+ if (!options.json) {
880
+ if (options.dryRun) {
881
+ console.log(pc.cyan("๐Ÿ” ไฟฎๅค้ข„่งˆ๏ผˆdry-run ๆจกๅผ๏ผŒไธไผšไฟฎๆ”นๆ–‡ไปถ๏ผ‰๏ผš"));
882
+ } else {
883
+ console.log(pc.cyan("๐Ÿ”ง ๆญฃๅœจๅบ”็”จ่‡ชๅŠจไฟฎๅค..."));
884
+ }
885
+ }
886
+ fixResult = engine.applyFixes(fixableIssues);
887
+ if (!options.json) {
888
+ if (options.dryRun && fixResult.previews) {
889
+ for (const preview of fixResult.previews) {
890
+ console.log(pc.cyan(`\n ๐Ÿ“„ ${preview.file}`));
891
+ console.log(pc.yellow(` ${preview.title}`));
892
+ console.log(preview.diff);
893
+ }
894
+ console.log(pc.blue(`\n ๐Ÿ’ก ๅ…ฑ ${fixResult.previews.length} ๅค„ๅฏไฟฎๅค๏ผˆไฝฟ็”จ --fix ๅบ”็”จ๏ผ‰`));
895
+ } else if (fixResult.filesModified.length > 0) {
896
+ console.log(
897
+ pc.green(
898
+ ` โœ… ๅทฒไฟฎๅค ${fixResult.fixedCount} ไธช้—ฎ้ข˜๏ผˆ${fixResult.filesModified.length} ไธชๆ–‡ไปถ๏ผ‰`
899
+ )
900
+ );
901
+ for (const f of fixResult.filesModified) {
902
+ console.log(pc.gray(` - ${f}`));
903
+ }
904
+ if (fixResult.skippedByUser && fixResult.skippedByUser > 0) {
905
+ console.log(pc.yellow(` โญ๏ธ ่ทณ่ฟ‡ ${fixResult.skippedByUser} ไธช้—ฎ้ข˜๏ผˆไฝŽ็ฝฎไฟกๅบฆๆˆ–็”จๆˆท้€‰ๆ‹ฉ๏ผ‰`));
906
+ }
907
+ } else {
908
+ console.log(pc.yellow(" โš ๏ธ ๆœชๅบ”็”จไปปไฝ•ไฟฎๅค"));
909
+ }
910
+ if (fixResult.errors.length > 0) {
911
+ for (const err of fixResult.errors) {
912
+ console.log(pc.red(` โŒ ${err}`));
913
+ }
914
+ }
915
+ console.log("");
916
+ }
917
+ }
918
+
919
+ // ๆ ผๅผๅŒ–๏ผˆ--format๏ผ‰
920
+ let formatResult = null;
921
+ if (options.format && !options.dryRun) {
922
+ const filesToFormat = options.fix && fixResult?.filesModified ? fixResult.filesModified : undefined;
923
+ if (!options.json) {
924
+ console.log(pc.cyan("๐ŸŽจ ๆญฃๅœจๆ ผๅผๅŒ–ไปฃ็ ..."));
925
+ }
926
+ formatResult = engine.format(filesToFormat);
927
+ if (!options.json) {
928
+ if (formatResult.formatted > 0) {
929
+ console.log(pc.green(` โœ… ${formatResult.formatter} ๅทฒๆ ผๅผๅŒ– ${formatResult.formatted} ไธชๆ–‡ไปถ`));
930
+ } else if (formatResult.errors.length === 0) {
931
+ console.log(pc.gray(" ๆ‰€ๆœ‰ๆ–‡ไปถๅทฒๆ˜ฏๆœ€ไผ˜ๆ ผๅผ"));
932
+ }
933
+ if (formatResult.errors.length > 0) {
934
+ for (const err of formatResult.errors) {
935
+ console.log(pc.red(` โŒ ${err}`));
936
+ }
937
+ }
938
+ console.log("");
939
+ }
940
+ }
941
+
942
+ // v2.6.0: ่‡ชๅŠจไฟฎๅค Bot
943
+ if (options.fixBot && fixResult?.filesModified?.length > 0) {
944
+ const botConfig = detectFixBotConfig();
945
+ if (botConfig) {
946
+ if (!options.json) {
947
+ console.log(pc.cyan("๐Ÿค– ๆญฃๅœจๅˆ›ๅปบไฟฎๅค PR..."));
948
+ }
949
+ const botResult = await runFixBot(options.projectDir, fixResult.filesModified, botConfig);
950
+ if (!options.json) {
951
+ if (botResult.success) {
952
+ console.log(pc.green(` โœ… ไฟฎๅค PR ๅทฒๅˆ›ๅปบ`));
953
+ console.log(pc.gray(` ๅˆ†ๆ”ฏ: ${botResult.branch}`));
954
+ if (botResult.prUrl) {
955
+ console.log(pc.gray(` ${botResult.prUrl}`));
956
+ }
957
+ } else {
958
+ console.log(pc.red(` โŒ Fix Bot ๅคฑ่ดฅ: ${botResult.error}`));
959
+ }
960
+ console.log("");
961
+ }
962
+ } else {
963
+ if (!options.json) {
964
+ console.log(
965
+ pc.yellow(
966
+ " โš ๏ธ ๆœชๆฃ€ๆต‹ๅˆฐ Fix Bot ้…็ฝฎใ€‚่ฏท่ฎพ็ฝฎ FG_FIX_BOT_PROVIDER ๅ’Œ FG_FIX_BOT_TOKEN ็Žฏๅขƒๅ˜้‡ใ€‚"
967
+ )
968
+ );
969
+ console.log("");
970
+ }
971
+ }
972
+ }
973
+
974
+ // v2.3.0: Baseline ๆจกๅผ
975
+ let allIssues = [...issues.critical, ...issues.warning, ...issues.suggestion];
976
+ if (options.baseline) {
977
+ if (options.generateBaseline) {
978
+ const baseline = new BaselineManager(options.baseline, options.projectDir);
979
+ baseline.save(allIssues, { projectDir: options.projectDir });
980
+ if (!options.json) {
981
+ console.log(pc.cyan(`๐Ÿ“‹ Baseline ๅทฒ็”Ÿๆˆ: ${baseline.getPath()}`));
982
+ console.log(pc.gray(` ๅŒ…ๅซ ${allIssues.length} ไธชๅทฒ็Ÿฅ้—ฎ้ข˜`));
983
+ }
984
+ process.exit(0);
985
+ }
986
+ const baseline = new BaselineManager(options.baseline, options.projectDir);
987
+ const baselineResult = baseline.filterNewIssues(allIssues);
988
+ if (baselineResult.baselineLoaded && !options.json) {
989
+ console.log(
990
+ pc.cyan(
991
+ `๐Ÿ“‹ Baseline ๆจกๅผ: ๅฟฝ็•ฅ ${baselineResult.knownIssues.length} ไธชๅทฒ็Ÿฅ้—ฎ้ข˜๏ผŒๅ…ณๆณจ ${baselineResult.newIssues.length} ไธชๆ–ฐๅขž้—ฎ้ข˜`
992
+ )
993
+ );
994
+ console.log("");
995
+ }
996
+ // ่ฟ‡ๆปค issues
997
+ const newKeySet = new Set(baselineResult.newIssues.map((i) => `${i.file}|${i.ruleId}|${i.line}`));
998
+ issues.critical = issues.critical.filter((i) => newKeySet.has(`${i.file}|${i.ruleId}|${i.line}`));
999
+ issues.warning = issues.warning.filter((i) => newKeySet.has(`${i.file}|${i.ruleId}|${i.line}`));
1000
+ issues.suggestion = issues.suggestion.filter((i) => newKeySet.has(`${i.file}|${i.ruleId}|${i.line}`));
1001
+ result.total = issues.critical.length + issues.warning.length + issues.suggestion.length;
1002
+ result.filesWithIssues = new Set([...issues.critical, ...issues.warning, ...issues.suggestion].map((i) => i.file)).size;
1003
+ allIssues = baselineResult.newIssues;
1004
+ }
1005
+
1006
+ // v2.3.0: SARIF ่พ“ๅ‡บ
1007
+ if (options.sarif) {
1008
+ const sarif = generateSarif(allIssues, {
1009
+ toolName: "Frontend Guardian",
1010
+ toolVersion: "2.3.0",
1011
+ projectDir: options.projectDir,
1012
+ });
1013
+ writeFileSync(options.sarif, JSON.stringify(sarif, null, 2), "utf-8");
1014
+ if (!options.json && !options.githubActions) {
1015
+ console.log(pc.cyan(`๐Ÿ“„ SARIF ๆŠฅๅ‘Šๅทฒไฟๅญ˜: ${options.sarif}`));
1016
+ console.log(pc.gray(` ๅŒ…ๅซ ${allIssues.length} ไธช้—ฎ้ข˜`));
1017
+ console.log("");
1018
+ }
1019
+ }
1020
+
1021
+ // v2.3.0: GitHub Actions Annotation
1022
+ if (options.githubActions || isGitHubActions()) {
1023
+ if (allIssues.length > 0) {
1024
+ console.log(formatAllAnnotations(allIssues));
1025
+ }
1026
+ writeJobSummary(allIssues, { totalFilesScanned: result.filesScanned, duration: result.duration });
1027
+ }
1028
+
1029
+ // v2.5.0: PR/MR ่ฏ„่ฎบ่‡ชๅŠจๅ‘ๅธƒ
1030
+ if (options.postComment) {
1031
+ const singleResult = { [options.module]: result };
1032
+ const commentBody = generatePRComment(
1033
+ singleResult,
1034
+ {
1035
+ timestamp: new Date().toISOString(),
1036
+ commitSha: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA,
1037
+ duration: result.duration,
1038
+ filesScanned: result.filesScanned,
1039
+ },
1040
+ {
1041
+ fixResult: fixResult
1042
+ ? { fixedCount: fixResult.fixedCount, filesModified: fixResult.filesModified }
1043
+ : null,
1044
+ }
1045
+ );
1046
+
1047
+ let pubConfig = detectPublisherConfig();
1048
+ if (!pubConfig && options.commentProvider) {
1049
+ const token =
1050
+ options.commentProvider === "github"
1051
+ ? process.env.GITHUB_TOKEN
1052
+ : process.env.GITLAB_TOKEN;
1053
+ const repo =
1054
+ options.commentProvider === "github"
1055
+ ? process.env.GITHUB_REPOSITORY
1056
+ : process.env.CI_PROJECT_ID;
1057
+ if (token && repo && options.prNumber) {
1058
+ pubConfig = {
1059
+ provider: options.commentProvider,
1060
+ token,
1061
+ repository: repo,
1062
+ prNumber: options.prNumber,
1063
+ };
1064
+ }
1065
+ }
1066
+
1067
+ if (pubConfig) {
1068
+ try {
1069
+ const publisher = createPublisher(pubConfig);
1070
+ const pubResult = await publisher.publish(commentBody);
1071
+ if (pubResult.success) {
1072
+ console.log(
1073
+ pc.cyan(`๐Ÿ’ฌ PR/MR ่ฏ„่ฎบๅทฒ${pubResult.action === "updated" ? "ๆ›ดๆ–ฐ" : "ๅ‘ๅธƒ"}`)
1074
+ );
1075
+ if (pubResult.commentUrl) {
1076
+ console.log(pc.gray(` ${pubResult.commentUrl}`));
1077
+ }
1078
+ } else {
1079
+ console.log(pc.red(` โŒ ่ฏ„่ฎบๅ‘ๅธƒๅคฑ่ดฅ: ${pubResult.error}`));
1080
+ }
1081
+ } catch (err) {
1082
+ console.log(pc.red(` โŒ ่ฏ„่ฎบๅ‘ๅธƒๅผ‚ๅธธ: ${err.message || err}`));
1083
+ }
1084
+ } else {
1085
+ console.log(
1086
+ pc.yellow(
1087
+ " โš ๏ธ ๆ— ๆณ•ๆฃ€ๆต‹ CI ็Žฏๅขƒ๏ผŒ่ทณ่ฟ‡่ฏ„่ฎบๅ‘ๅธƒใ€‚่ฏทๆฃ€ๆŸฅ็Žฏๅขƒๅ˜้‡๏ผŒๆˆ–ๆ˜พๅผๆŒ‡ๅฎš --comment-provider ๅ’Œ --pr-number"
1088
+ )
1089
+ );
1090
+ }
1091
+ console.log("");
1092
+ }
1093
+
1094
+ if (options.json) {
1095
+ const jsonOutput = {
1096
+ ...result,
1097
+ fix: fixResult,
1098
+ format: formatResult,
1099
+ };
1100
+ console.log(JSON.stringify(jsonOutput, null, 2));
1101
+ process.exit(issues.critical.length > 0 ? 1 : 0);
1102
+ }
1103
+
1104
+ // ็ปˆ็ซฏ่พ“ๅ‡บ๏ผˆbaseline ่ฟ‡ๆปคๅŽ้‡ๆ–ฐ่ฎก็ฎ— totals๏ผ‰
1105
+ totalCritical = issues.critical.length;
1106
+ totalWarning = issues.warning.length;
1107
+ totalSuggestion = issues.suggestion.length;
1108
+
1109
+ if (totalCritical + totalWarning + totalSuggestion === 0) {
1110
+ console.log(pc.green("โœ… ๆœชๅ‘็Žฐ้—ฎ้ข˜"));
1111
+ } else {
1112
+ console.log(pc.red(`๐Ÿ”ด Critical: ${totalCritical}`));
1113
+ console.log(pc.yellow(`๐ŸŸก Warning: ${totalWarning}`));
1114
+ console.log(pc.blue(`๐Ÿ’ก Suggestion: ${totalSuggestion}`));
1115
+ console.log("");
1116
+
1117
+ const allIssues = [...issues.critical, ...issues.warning, ...issues.suggestion];
1118
+
1119
+ for (const issue of allIssues) {
1120
+ printIssue(
1121
+ issue,
1122
+ issue.severity === "critical" ? pc.red : issue.severity === "warning" ? pc.yellow : pc.blue
1123
+ );
1124
+ }
1125
+ }
1126
+
1127
+ console.log(
1128
+ pc.gray(
1129
+ `โฑ๏ธ ่€—ๆ—ถ: ${result.duration}ms | ๆ‰ซๆ ${result.filesScanned} ไธชๆ–‡ไปถ | ${result.filesWithIssues} ไธชๆ–‡ไปถๆœ‰้—ฎ้ข˜`
1130
+ )
1131
+ );
1132
+
1133
+ if (totalCritical > 0) {
1134
+ process.exit(1);
1135
+ }
1136
+ } catch (err) {
1137
+ console.error(pc.red("โŒ ๆ‰ซๆๅคฑ่ดฅ:"), err);
1138
+ process.exit(1);
1139
+ }
1140
+ }
1141
+
1142
+ function printIssue(issue, colorFn) {
1143
+ console.log(colorFn(` [${issue.severity.toUpperCase()}] ${issue.title}`));
1144
+ console.log(` ๐Ÿ“„ ${issue.file}:${issue.line}:${issue.column}`);
1145
+ console.log(` ${issue.description}`);
1146
+ if (issue.source) {
1147
+ console.log(pc.gray(` > ${issue.source}`));
1148
+ }
1149
+ if (issue.docsUrl) {
1150
+ console.log(pc.blue(` ๐Ÿ“– ${issue.docsUrl}`));
1151
+ }
1152
+ console.log("");
1153
+ }
1154
+
1155
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1156
+ // v2.3.0: Baseline / SARIF / GitHub Actions ่พ…ๅŠฉๅ‡ฝๆ•ฐ
1157
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1158
+
1159
+ /** ไปŽ allResults ๆ”ถ้›†ๆ‰€ๆœ‰ issues๏ผˆๅซๅค–้ƒจๅทฅๅ…ท๏ผ‰ */
1160
+ function collectAllIssues(allResults, externalResults) {
1161
+ const issues = [];
1162
+ for (const mod of MODULES) {
1163
+ const r = allResults[mod];
1164
+ if (r) {
1165
+ issues.push(...r.issues.critical, ...r.issues.warning, ...r.issues.suggestion);
1166
+ }
1167
+ }
1168
+ for (const er of externalResults) {
1169
+ issues.push(...er.issues);
1170
+ }
1171
+ return issues;
1172
+ }
1173
+
1174
+ /** ๅบ”็”จ baseline ่ฟ‡ๆปคๅˆฐ allResults๏ผŒ่ฟ”ๅ›ž BaselineResult */
1175
+ function applyBaselineToResults(allResults, externalResults, baselinePath, projectDir) {
1176
+ const allIssues = collectAllIssues(allResults, externalResults);
1177
+ const baseline = new BaselineManager(baselinePath, projectDir);
1178
+ const baselineResult = baseline.filterNewIssues(allIssues);
1179
+
1180
+ if (!baselineResult.baselineLoaded) {
1181
+ return baselineResult;
1182
+ }
1183
+
1184
+ const newKeySet = new Set(
1185
+ baselineResult.newIssues.map((i) => `${i.file}|${i.ruleId}|${i.line}`)
1186
+ );
1187
+
1188
+ function isNew(issue) {
1189
+ return newKeySet.has(`${issue.file}|${issue.ruleId}|${issue.line}`);
1190
+ }
1191
+
1192
+ // ่ฟ‡ๆปคๆฏไธชๆจกๅ—็š„ issues
1193
+ for (const mod of MODULES) {
1194
+ const r = allResults[mod];
1195
+ if (!r) continue;
1196
+ r.issues.critical = r.issues.critical.filter(isNew);
1197
+ r.issues.warning = r.issues.warning.filter(isNew);
1198
+ r.issues.suggestion = r.issues.suggestion.filter(isNew);
1199
+ r.total = r.issues.critical.length + r.issues.warning.length + r.issues.suggestion.length;
1200
+ r.filesWithIssues =
1201
+ new Set([
1202
+ ...r.issues.critical,
1203
+ ...r.issues.warning,
1204
+ ...r.issues.suggestion,
1205
+ ].map((i) => i.file)).size;
1206
+ }
1207
+
1208
+ // ่ฟ‡ๆปคๅค–้ƒจๅทฅๅ…ท issues
1209
+ for (const er of externalResults) {
1210
+ er.issues = er.issues.filter(isNew);
1211
+ }
1212
+
1213
+ return baselineResult;
1214
+ }
1215
+
1216
+ /** ้‡ๆ–ฐ่ฎก็ฎ— totals */
1217
+ function recalculateTotals(allResults, externalResults) {
1218
+ let critical = 0;
1219
+ let warning = 0;
1220
+ let suggestion = 0;
1221
+ for (const mod of MODULES) {
1222
+ const r = allResults[mod];
1223
+ if (!r) continue;
1224
+ critical += r.issues.critical.length;
1225
+ warning += r.issues.warning.length;
1226
+ suggestion += r.issues.suggestion.length;
1227
+ }
1228
+ for (const er of externalResults) {
1229
+ for (const issue of er.issues) {
1230
+ if (issue.severity === "critical") critical++;
1231
+ else if (issue.severity === "warning") warning++;
1232
+ else suggestion++;
1233
+ }
1234
+ }
1235
+ return { critical, warning, suggestion };
1236
+ }
1237
+
1238
+ main();