@nitra/cursor 1.8.103 → 1.8.105
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/bin/auto-rules.md +45 -0
- package/bin/n-cursor.js +280 -149
- package/mdc/graphql.mdc +15 -1
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +54 -36
- package/scripts/check-bun.mjs +2 -6
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +15 -11
- package/scripts/check-k8s.mjs +1652 -627
- package/scripts/check-nginx-default-tpl.mjs +17 -10
- package/scripts/check-npm-module.mjs +3 -3
- package/scripts/check-text.mjs +1 -3
- package/scripts/check-vue.mjs +2 -2
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +90 -72
- package/scripts/utils/workspaces.mjs +39 -16
package/schemas/n-cursor.json
CHANGED
|
@@ -27,6 +27,22 @@
|
|
|
27
27
|
"minLength": 1
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
+
"disable-rules": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"description": "Список rule id (без префікса n-), які не потрібно автододавати в `.n-cursor.json` під час запуску CLI.",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"disable-skills": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "Список skill id (без префікса n-), які не потрібно автододавати в `.n-cursor.json` під час запуску CLI.",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1
|
|
44
|
+
}
|
|
45
|
+
},
|
|
30
46
|
"version": {
|
|
31
47
|
"type": "string",
|
|
32
48
|
"description": "Застаріле поле, ігнорується CLI. Правила завжди копіюються з каталогу mdc/ установленого пакету (node_modules або кеш npx); змініть версію через оновлення залежності."
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Автовизначення правил і skills для `.n-cursor.json` за умовами з `npm/bin/auto-rules.md`.
|
|
3
|
+
*
|
|
4
|
+
* Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source, кореневий
|
|
5
|
+
* `package.json`) та повертає ідентифікатори правил і skills, які потрібно автододати.
|
|
6
|
+
*
|
|
7
|
+
* Також враховує винятки `disable-rules` і `disable-skills`: елементи з цих списків не
|
|
8
|
+
* додаються автоматично.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
12
|
+
import { basename, join, relative } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
isGqlScanSourceFile,
|
|
16
|
+
shouldSkipFileForGqlScan,
|
|
17
|
+
sourceFileHasGqlTaggedTemplate
|
|
18
|
+
} from './utils/graphql-gql-scan.mjs'
|
|
19
|
+
|
|
20
|
+
/** Порядок автододавання правил відповідно до `auto-rules.md`. */
|
|
21
|
+
export const AUTO_RULE_ORDER = Object.freeze([
|
|
22
|
+
'abie',
|
|
23
|
+
'bun',
|
|
24
|
+
'docker',
|
|
25
|
+
'ga',
|
|
26
|
+
'graphql',
|
|
27
|
+
'js-lint',
|
|
28
|
+
'js-pino',
|
|
29
|
+
'k8s',
|
|
30
|
+
'nginx-default-tpl',
|
|
31
|
+
'npm-module',
|
|
32
|
+
'style-lint',
|
|
33
|
+
'text',
|
|
34
|
+
'vue'
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/** Порядок автододавання skills відповідно до `auto-rules.md`. */
|
|
38
|
+
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
39
|
+
|
|
40
|
+
const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
|
|
41
|
+
const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/iu
|
|
42
|
+
const STYLE_RE = /\.(?:css|vue)$/iu
|
|
43
|
+
const VUE_RE = /\.vue$/iu
|
|
44
|
+
const NGINX_DEFAULT_FILES = new Set(['default.conf.template', 'default.conf', 'nginx.conf'])
|
|
45
|
+
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
46
|
+
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Фіксує ознаки, що залежать лише від імені підкаталогу.
|
|
50
|
+
* @param {string} dirName імʼя каталогу
|
|
51
|
+
* @param {{
|
|
52
|
+
* hasK8sDir: boolean,
|
|
53
|
+
* hasTempoDir: boolean
|
|
54
|
+
* }} facts агреговані факти
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
function updateDirFacts(dirName, facts) {
|
|
58
|
+
if (dirName === 'k8s') {
|
|
59
|
+
facts.hasK8sDir = true
|
|
60
|
+
}
|
|
61
|
+
if (dirName === 'tempo') {
|
|
62
|
+
facts.hasTempoDir = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Фіксує ознаки, що визначаються за шляхом/іменем файлу.
|
|
68
|
+
* @param {string} fileName базове імʼя файлу
|
|
69
|
+
* @param {string} relPath шлях відносно кореня
|
|
70
|
+
* @param {{
|
|
71
|
+
* hasDockerfile: boolean,
|
|
72
|
+
* hasJsLikeSource: boolean,
|
|
73
|
+
* hasNginxDefaultTplFile: boolean,
|
|
74
|
+
* hasVueOrCssSource: boolean,
|
|
75
|
+
* hasVueSource: boolean
|
|
76
|
+
* }} facts агреговані факти
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
function updateFileFacts(fileName, relPath, facts) {
|
|
80
|
+
if (fileName === 'Dockerfile' || fileName.startsWith('Dockerfile.')) {
|
|
81
|
+
facts.hasDockerfile = true
|
|
82
|
+
}
|
|
83
|
+
if (NGINX_DEFAULT_FILES.has(fileName)) {
|
|
84
|
+
facts.hasNginxDefaultTplFile = true
|
|
85
|
+
}
|
|
86
|
+
if (JS_LIKE_RE.test(relPath)) {
|
|
87
|
+
facts.hasJsLikeSource = true
|
|
88
|
+
}
|
|
89
|
+
if (VUE_RE.test(relPath)) {
|
|
90
|
+
facts.hasVueSource = true
|
|
91
|
+
}
|
|
92
|
+
if (STYLE_RE.test(relPath)) {
|
|
93
|
+
facts.hasVueOrCssSource = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Чи потрібно сканувати файл на gql tagged template.
|
|
99
|
+
* @param {string} relPath шлях відносно кореня
|
|
100
|
+
* @param {{ hasGqlTaggedTemplates: boolean }} facts агреговані факти
|
|
101
|
+
* @returns {boolean} true, якщо файл варто сканувати
|
|
102
|
+
*/
|
|
103
|
+
function shouldScanFileForGql(relPath, facts) {
|
|
104
|
+
return !facts.hasGqlTaggedTemplates && isGqlScanSourceFile(relPath) && !shouldSkipFileForGqlScan(relPath)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Оновлює ознаку `hasGqlTaggedTemplates` за вмістом конкретного файлу.
|
|
109
|
+
* @param {string} absPath абсолютний шлях до файлу
|
|
110
|
+
* @param {string} relPath шлях відносно кореня
|
|
111
|
+
* @param {{ hasGqlTaggedTemplates: boolean }} facts агреговані факти
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
*/
|
|
114
|
+
async function updateGqlFactFromFile(absPath, relPath, facts) {
|
|
115
|
+
try {
|
|
116
|
+
const content = await readFile(absPath, 'utf8')
|
|
117
|
+
if (sourceFileHasGqlTaggedTemplate(content, relPath)) {
|
|
118
|
+
facts.hasGqlTaggedTemplates = true
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* ігноруємо пошкоджені/недоступні файли */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Обробляє файл під час обходу дерева.
|
|
127
|
+
* @param {string} absPath абсолютний шлях до файлу
|
|
128
|
+
* @param {string} root абсолютний шлях кореня
|
|
129
|
+
* @param {{
|
|
130
|
+
* hasDockerfile: boolean,
|
|
131
|
+
* hasGqlTaggedTemplates: boolean,
|
|
132
|
+
* hasJsLikeSource: boolean,
|
|
133
|
+
* hasNginxDefaultTplFile: boolean,
|
|
134
|
+
* hasVueOrCssSource: boolean,
|
|
135
|
+
* hasVueSource: boolean
|
|
136
|
+
* }} facts агреговані факти
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
*/
|
|
139
|
+
async function processFileEntry(absPath, root, facts) {
|
|
140
|
+
const rel = relative(root, absPath).split('\\').join('/')
|
|
141
|
+
const fileName = basename(absPath)
|
|
142
|
+
updateFileFacts(fileName, rel, facts)
|
|
143
|
+
if (shouldScanFileForGql(rel, facts)) {
|
|
144
|
+
await updateGqlFactFromFile(absPath, rel, facts)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Нормалізує список ідентифікаторів (trim + lowercase + унікальність збереженням порядку).
|
|
150
|
+
* @param {unknown} value вихідне значення з `.n-cursor.json`
|
|
151
|
+
* @returns {string[]} масив id у нормалізованому вигляді
|
|
152
|
+
*/
|
|
153
|
+
export function normalizeIdList(value) {
|
|
154
|
+
if (!Array.isArray(value)) {
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
const out = []
|
|
158
|
+
for (const item of value) {
|
|
159
|
+
const normalized = String(item).trim().toLowerCase()
|
|
160
|
+
if (normalized && !out.includes(normalized)) {
|
|
161
|
+
out.push(normalized)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Повертає URL репозиторію з package.json (`repository` може бути рядком або обʼєктом).
|
|
169
|
+
* @param {unknown} repository значення `packageJson.repository`
|
|
170
|
+
* @returns {string | null} URL або null
|
|
171
|
+
*/
|
|
172
|
+
export function getRepositoryUrl(repository) {
|
|
173
|
+
if (typeof repository === 'string') {
|
|
174
|
+
return repository
|
|
175
|
+
}
|
|
176
|
+
if (repository && typeof repository === 'object' && !Array.isArray(repository)) {
|
|
177
|
+
const url = /** @type {Record<string, unknown>} */ (repository).url
|
|
178
|
+
if (typeof url === 'string') {
|
|
179
|
+
return url
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Чи package.json виглядає як монорепо (поле `workspaces`).
|
|
187
|
+
* @param {unknown} packageJson кореневий package.json як JS-обʼєкт
|
|
188
|
+
* @returns {boolean} true, якщо оголошено workspaces
|
|
189
|
+
*/
|
|
190
|
+
export function isMonorepoPackage(packageJson) {
|
|
191
|
+
if (packageJson === null || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
const workspaces = /** @type {Record<string, unknown>} */ (packageJson).workspaces
|
|
195
|
+
if (Array.isArray(workspaces)) {
|
|
196
|
+
return workspaces.length > 0
|
|
197
|
+
}
|
|
198
|
+
if (workspaces && typeof workspaces === 'object' && !Array.isArray(workspaces)) {
|
|
199
|
+
const packages = /** @type {Record<string, unknown>} */ (workspaces).packages
|
|
200
|
+
return Array.isArray(packages) && packages.length > 0
|
|
201
|
+
}
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Обходить дерево проєкту, збираючи факти для автоувімкнення правил.
|
|
207
|
+
* @param {string} root абсолютний шлях кореня репозиторію
|
|
208
|
+
* @returns {Promise<{
|
|
209
|
+
* hasDockerfile: boolean,
|
|
210
|
+
* hasGaWorkflowsDir: boolean,
|
|
211
|
+
* hasGqlTaggedTemplates: boolean,
|
|
212
|
+
* hasJsLikeSource: boolean,
|
|
213
|
+
* hasK8sDir: boolean,
|
|
214
|
+
* hasNginxDefaultTplFile: boolean,
|
|
215
|
+
* hasTempoDir: boolean,
|
|
216
|
+
* hasVueSource: boolean,
|
|
217
|
+
* hasVueOrCssSource: boolean
|
|
218
|
+
* }>} агреговані факти
|
|
219
|
+
*/
|
|
220
|
+
export async function collectAutoRuleFacts(root) {
|
|
221
|
+
const facts = {
|
|
222
|
+
hasDockerfile: false,
|
|
223
|
+
hasGaWorkflowsDir: existsSync(join(root, '.github', 'workflows')),
|
|
224
|
+
hasGqlTaggedTemplates: false,
|
|
225
|
+
hasJsLikeSource: false,
|
|
226
|
+
hasK8sDir: false,
|
|
227
|
+
hasNginxDefaultTplFile: false,
|
|
228
|
+
hasTempoDir: false,
|
|
229
|
+
hasVueSource: false,
|
|
230
|
+
hasVueOrCssSource: false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Рекурсивний обхід каталогу з пропуском службових директорій.
|
|
235
|
+
* @param {string} dir абсолютний шлях каталогу
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async function walk(dir) {
|
|
239
|
+
let entries
|
|
240
|
+
try {
|
|
241
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
242
|
+
} catch {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const absPath = join(dir, entry.name)
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
const isIgnoredDir = IGNORED_DIR_NAMES.has(entry.name)
|
|
250
|
+
if (!isIgnoredDir) {
|
|
251
|
+
updateDirFacts(entry.name, facts)
|
|
252
|
+
await walk(absPath)
|
|
253
|
+
}
|
|
254
|
+
} else if (entry.isFile()) {
|
|
255
|
+
await processFileEntry(absPath, root, facts)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await walk(root)
|
|
261
|
+
return facts
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Визначає авто-правила та skills згідно з `auto-rules.md`.
|
|
266
|
+
* @param {object} params параметри аналізу
|
|
267
|
+
* @param {string} params.root абсолютний шлях до кореня репозиторію
|
|
268
|
+
* @param {string[]} params.availableRules перелік доступних правил з пакету
|
|
269
|
+
* @param {string[]} params.availableSkills перелік доступних skills з пакету
|
|
270
|
+
* @param {unknown} params.packageJsonParsed кореневий package.json (розпарсений) або null
|
|
271
|
+
* @param {string[]} [params.disableRules] список `disable-rules` з конфігу
|
|
272
|
+
* @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
|
|
273
|
+
* @returns {Promise<{ rules: string[], skills: string[] }>} списки id у стабільному порядку
|
|
274
|
+
*/
|
|
275
|
+
export async function detectAutoRulesAndSkills({
|
|
276
|
+
root,
|
|
277
|
+
availableRules,
|
|
278
|
+
availableSkills,
|
|
279
|
+
packageJsonParsed,
|
|
280
|
+
disableRules = DEFAULT_DISABLED_LIST,
|
|
281
|
+
disableSkills = DEFAULT_DISABLED_LIST
|
|
282
|
+
}) {
|
|
283
|
+
const facts = await collectAutoRuleFacts(root)
|
|
284
|
+
const normalizedRules = new Set(availableRules.map(r => r.trim().toLowerCase()))
|
|
285
|
+
const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
|
|
286
|
+
const disableRulesSet = new Set(disableRules)
|
|
287
|
+
const disableSkillsSet = new Set(disableSkills)
|
|
288
|
+
|
|
289
|
+
const packageJsonExists = existsSync(join(root, 'package.json'))
|
|
290
|
+
const npmDirExists = existsSync(join(root, 'npm'))
|
|
291
|
+
const repositoryUrl = getRepositoryUrl(
|
|
292
|
+
packageJsonParsed && typeof packageJsonParsed === 'object' && !Array.isArray(packageJsonParsed)
|
|
293
|
+
? /** @type {Record<string, unknown>} */ (packageJsonParsed).repository
|
|
294
|
+
: null
|
|
295
|
+
)
|
|
296
|
+
const isAbie = typeof repositoryUrl === 'string' && repositoryUrl.toLowerCase().includes(ABIE_REPOSITORY_URL_MARKER)
|
|
297
|
+
const isMonorepo = isMonorepoPackage(packageJsonParsed)
|
|
298
|
+
|
|
299
|
+
/** @type {string[]} */
|
|
300
|
+
const detectedRules = []
|
|
301
|
+
/** @type {string[]} */
|
|
302
|
+
const detectedSkills = []
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Додає правило до результату, якщо воно доступне і не в disable-списку.
|
|
306
|
+
* @param {string} ruleId id правила
|
|
307
|
+
* @returns {void}
|
|
308
|
+
*/
|
|
309
|
+
function addRule(ruleId) {
|
|
310
|
+
if (!normalizedRules.has(ruleId) || disableRulesSet.has(ruleId) || detectedRules.includes(ruleId)) {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
detectedRules.push(ruleId)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Додає skill до результату, якщо він доступний і не в disable-списку.
|
|
318
|
+
* @param {string} skillId id skill
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
321
|
+
function addSkill(skillId) {
|
|
322
|
+
if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detectedSkills.includes(skillId)) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
detectedSkills.push(skillId)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const autoRuleChecks = [
|
|
329
|
+
{ enabled: isAbie, id: 'abie' },
|
|
330
|
+
{ enabled: packageJsonExists, id: 'bun' },
|
|
331
|
+
{ enabled: facts.hasDockerfile, id: 'docker' },
|
|
332
|
+
{ enabled: facts.hasGaWorkflowsDir, id: 'ga' },
|
|
333
|
+
{ enabled: facts.hasGqlTaggedTemplates, id: 'graphql' },
|
|
334
|
+
{ enabled: facts.hasJsLikeSource, id: 'js-lint' },
|
|
335
|
+
{ enabled: facts.hasJsLikeSource && !(isMonorepo && facts.hasVueSource && facts.hasTempoDir), id: 'js-pino' },
|
|
336
|
+
{ enabled: facts.hasK8sDir, id: 'k8s' },
|
|
337
|
+
{ enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
|
|
338
|
+
{ enabled: npmDirExists, id: 'npm-module' },
|
|
339
|
+
{ enabled: facts.hasVueOrCssSource, id: 'style-lint' }
|
|
340
|
+
]
|
|
341
|
+
for (const item of autoRuleChecks) {
|
|
342
|
+
if (item.enabled) {
|
|
343
|
+
addRule(item.id)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
addRule('text')
|
|
347
|
+
if (facts.hasVueSource) {
|
|
348
|
+
addRule('vue')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const autoSkillChecks = [
|
|
352
|
+
{ enabled: isAbie, id: 'abie-kustomize' },
|
|
353
|
+
{ enabled: true, id: 'fix' },
|
|
354
|
+
{ enabled: true, id: 'lint' }
|
|
355
|
+
]
|
|
356
|
+
for (const item of autoSkillChecks) {
|
|
357
|
+
if (item.enabled) {
|
|
358
|
+
addSkill(item.id)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const rules = AUTO_RULE_ORDER.filter(ruleId => detectedRules.includes(ruleId))
|
|
363
|
+
const skills = AUTO_SKILL_ORDER.filter(skillId => detectedSkills.includes(skillId))
|
|
364
|
+
return { rules, skills }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Доповнює конфіг автодетектом (лише додає; існуючі вручну задані елементи не прибирає).
|
|
369
|
+
* @param {object} params параметри оновлення
|
|
370
|
+
* @param {{ rules: unknown, skills?: unknown, ['disable-rules']?: unknown, ['disable-skills']?: unknown }} params.config розпарсений `.n-cursor.json`
|
|
371
|
+
* @param {string[]} params.detectedRules правила, визначені автодетектом
|
|
372
|
+
* @param {string[]} params.detectedSkills skills, визначені автодетектом
|
|
373
|
+
* @returns {{ rules: string[], skills: string[] } & Record<string, unknown>} новий нормалізований конфіг
|
|
374
|
+
*/
|
|
375
|
+
export function mergeConfigWithAutoDetected({ config, detectedRules, detectedSkills }) {
|
|
376
|
+
const existingRules = normalizeIdList(config.rules)
|
|
377
|
+
const existingSkills = normalizeIdList(config.skills)
|
|
378
|
+
const disableRules = normalizeIdList(config['disable-rules'])
|
|
379
|
+
const disableSkills = normalizeIdList(config['disable-skills'])
|
|
380
|
+
|
|
381
|
+
const rules = [...existingRules]
|
|
382
|
+
for (const id of detectedRules) {
|
|
383
|
+
if (!rules.includes(id) && !disableRules.includes(id)) {
|
|
384
|
+
rules.push(id)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const skills = [...existingSkills]
|
|
389
|
+
for (const id of detectedSkills) {
|
|
390
|
+
if (!skills.includes(id) && !disableSkills.includes(id)) {
|
|
391
|
+
skills.push(id)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @type {{ rules: string[], skills: string[] } & Record<string, unknown>} */
|
|
396
|
+
const normalized = { rules, skills }
|
|
397
|
+
if (disableRules.length > 0) {
|
|
398
|
+
normalized['disable-rules'] = disableRules
|
|
399
|
+
}
|
|
400
|
+
if (disableSkills.length > 0) {
|
|
401
|
+
normalized['disable-skills'] = disableSkills
|
|
402
|
+
}
|
|
403
|
+
return normalized
|
|
404
|
+
}
|