@nitra/cursor 1.8.171 → 1.8.173

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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.173] - 2026-05-04
8
+
9
+ ### Added
10
+
11
+ - `.n-cursor.json` поле `ignore` (`schemas/n-cursor.json`): тепер не лише сигнал для AI, а й керує обходом усіх `check-*.mjs` / `run-*.mjs` — перелічені каталоги повністю виключаються з `walkDir`, як `node_modules` чи `.git`. Дозволяє безпечно тримати vendored Helm-чарти, генеровані маніфести, legacy-дерева у репо без false-positive’ів від check-скриптів. Розширено опис у схемі (стандартні виключення додавати не треба) і README отримав секцію «Виключення цілих дерев».
12
+ - `scripts/utils/load-cursor-config.mjs`: нова утиліта `loadCursorIgnorePaths(root)` — читає поле `ignore` з `.n-cursor.json` і нормалізує до абсолютних posix-шляхів без trailing-slash; пропускає не-рядки та порожні елементи; повертає `[]`, якщо файлу/поля нема або JSON невалідний.
13
+ - `scripts/utils/walkDir.mjs`: третій аргумент `ignorePaths` (за замовчуванням `[]`) — каталоги, які пропускаються разом з усім вмістом. Збіг — за повним шляхом (точний або з префіксом `/`), а не за basename, тож `postgres-master-test/` не пропускається коли в ignore лише `postgres-master/`. Стандартні пропуски (`node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`) працюють як раніше.
14
+
15
+ ### Changed
16
+
17
+ - Усі скрипти, що обходять FS через `walkDir`, тепер на початку `check()` зчитують `loadCursorIgnorePaths(root)` і передають третім аргументом: `check-abie`, `check-docker`, `check-graphql`, `check-hasura`, `check-image`, `check-js-bun-db`, `check-js-mssql`, `check-js-run`, `check-k8s`, `check-nginx-default-tpl`, `check-npm-module`, `check-vue`, плюс `run-docker`, `run-k8s` і `rename-yaml-extensions`. Wrapper-функції (`findDockerfilePaths`, `findK8sYamlFiles`, `findLintDockerfilePaths`, `findK8sRoots`, `findDefaultConfTemplatePaths`, `migrateDefaultTplConfFiles`) отримали опційний параметр `ignorePaths` для прозорого пробросу.
18
+
19
+ ## [1.8.172] - 2026-05-04
20
+
21
+ ### Changed
22
+
23
+ - `auto-rules.md` / `auto-rules.mjs`: правило `php` тепер автоувімкається за наявністю `composer.json` у корені, а не за будь-яким `*.php` файлом у дереві. Прибрано константу `PHP_RE`, факт `hasPhpSource` і його збір у `updateFileFacts`/`collectAutoRuleFacts`; натомість у `detectAutoRulesAndSkills` додано прапорець `composerJsonExists` (за аналогією з `packageJsonExists` / `npmDirExists`).
24
+
7
25
  ## [1.8.171] - 2026-05-04
8
26
 
9
27
  ### Removed
package/README.md CHANGED
@@ -30,6 +30,20 @@
30
30
 
31
31
  Щоб використовувати конкретну версію правил, оновіть залежність `@nitra/cursor` у проєкті (`bun add -d @nitra/cursor@<версія>` тощо). Поле `version` у `.n-cursor.json`, якщо воно лишилось у старих конфігах, **ігнорується**.
32
32
 
33
+ ### Виключення цілих дерев — поле `ignore`
34
+
35
+ Поле `ignore` у `.n-cursor.json` — список директорій (posix-шляхи відносно кореня репозиторію), які CLI повністю пропускає під час обходу: жоден `check-*.mjs` не сканує і не валідує файли всередині них, а агент не редагує/не створює/не видаляє там файли. Стандартні виключення (`node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`) працюють завжди — додавати їх у `ignore` не потрібно.
36
+
37
+ Типові кандидати: vendored Helm-чарти, генеровані маніфести, legacy-дерева, які не підтягуються під поточні правила:
38
+
39
+ ```json
40
+ {
41
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
42
+ "rules": ["k8s"],
43
+ "ignore": ["dremio/dev/dremio_v2", "postgres-master"]
44
+ }
45
+ ```
46
+
33
47
  ### Правило `k8s` і Kustomize
34
48
 
35
49
  У цільовому репозиторії з маніфестами під **`**/k8s`** дотримуйтесь **`mdc/k8s.mdc`** з пакету (після синку — `.cursor/rules/n-k8s.mdc`або копія з`node_modules/@nitra/cursor/mdc/k8s.mdc`).
package/bin/auto-rules.md CHANGED
@@ -36,7 +36,7 @@ nginx-default-tpl - якщо присутній хоч один файл з пе
36
36
 
37
37
  npm-module - якщо в корені присутня директорія npm
38
38
 
39
- php - якщо присутній хоч один php файл
39
+ php - якщо в корені є composer.json
40
40
 
41
41
  style-lint - якщо присутній хоч один vue або css файл
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.171",
3
+ "version": "1.8.173",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "ignore": {
47
47
  "type": "array",
48
- "description": "Директорії, у яких заборонено будь-які модифікації файлів (AI не редагує, не видаляє, не створює файли). Шляхи відносно кореня репозиторію. Наприклад: [\"packages/vendor\", \"packages/generated\"].",
48
+ "description": "Директорії, що повністю виключаються з обходу check-скриптів CLI (npx @nitra/cursor check ...) і AI-модифікацій (AI не редагує, не видаляє, не створює файли). Шляхи відносно кореня репозиторію (posix). Приклади: vendored Helm-чарти, генеровані маніфести, legacy-дерева. Стандартні виключення (node_modules, .git, dist, coverage, .turbo, .next) застосовуються завжди — додавати їх не треба.",
49
49
  "items": {
50
50
  "type": "string",
51
51
  "minLength": 1
@@ -63,7 +63,6 @@ const HASURA_CONFIG_MARKER = 'metadata_directory: metadata'
63
63
  const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/iu
64
64
  const STYLE_RE = /\.(?:css|vue)$/iu
65
65
  const VUE_RE = /\.vue$/iu
66
- const PHP_RE = /\.php$/iu
67
66
  const NGINX_DEFAULT_FILES = new Set(['default.conf.template', 'default.conf', 'nginx.conf'])
68
67
  const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
69
68
  const DEFAULT_DISABLED_LIST = Object.freeze([])
@@ -245,7 +244,6 @@ function updateDirFacts(dirName, facts) {
245
244
  * hasDockerfile: boolean,
246
245
  * hasJsLikeSource: boolean,
247
246
  * hasNginxDefaultTplFile: boolean,
248
- * hasPhpSource: boolean,
249
247
  * hasVueOrCssSource: boolean,
250
248
  * hasVueSource: boolean
251
249
  * }} facts агреговані факти
@@ -267,9 +265,6 @@ function updateFileFacts(fileName, relPath, facts) {
267
265
  if (VUE_RE.test(relPath)) {
268
266
  facts.hasVueSource = true
269
267
  }
270
- if (PHP_RE.test(relPath)) {
271
- facts.hasPhpSource = true
272
- }
273
268
  if (STYLE_RE.test(relPath)) {
274
269
  facts.hasVueOrCssSource = true
275
270
  }
@@ -452,7 +447,6 @@ export function isMonorepoPackage(packageJson) {
452
447
  * hasK8sDir: boolean,
453
448
  * hasNginxDefaultTplFile: boolean,
454
449
  * hasTempoDir: boolean,
455
- * hasPhpSource: boolean,
456
450
  * hasVueSource: boolean,
457
451
  * hasVueOrCssSource: boolean
458
452
  * }>} агреговані факти
@@ -469,7 +463,6 @@ export async function collectAutoRuleFacts(root) {
469
463
  hasK8sDir: false,
470
464
  hasNginxDefaultTplFile: false,
471
465
  hasTempoDir: false,
472
- hasPhpSource: false,
473
466
  hasVueSource: false,
474
467
  hasVueOrCssSource: false
475
468
  }
@@ -556,6 +549,7 @@ export async function detectAutoRulesAndSkills({
556
549
 
557
550
  const packageJsonExists = existsSync(join(root, 'package.json'))
558
551
  const npmDirExists = existsSync(join(root, 'npm'))
552
+ const composerJsonExists = existsSync(join(root, 'composer.json'))
559
553
  const repositoryUrl = getRepositoryUrl(
560
554
  packageJsonParsed && typeof packageJsonParsed === 'object' && !Array.isArray(packageJsonParsed)
561
555
  ? /** @type {Record<string, unknown>} */ (packageJsonParsed).repository
@@ -612,7 +606,7 @@ export async function detectAutoRulesAndSkills({
612
606
  { enabled: facts.hasK8sDir, id: 'k8s' },
613
607
  { enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
614
608
  { enabled: npmDirExists, id: 'npm-module' },
615
- { enabled: facts.hasPhpSource, id: 'php' },
609
+ { enabled: composerJsonExists, id: 'php' },
616
610
  { enabled: facts.hasVueOrCssSource, id: 'style-lint' }
617
611
  ]
618
612
  for (const item of autoRuleChecks) {
@@ -47,6 +47,7 @@ import { parseAllDocuments } from 'yaml'
47
47
  import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
48
48
  import { createCheckReporter } from './utils/check-reporter.mjs'
49
49
  import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
50
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
50
51
  import { walkDir } from './utils/walkDir.mjs'
51
52
 
52
53
  const CONFIG_FILE = '.n-cursor.json'
@@ -385,18 +386,22 @@ export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
385
386
  * @param {string} root корінь репозиторію
386
387
  * @returns {Promise<string[]>} відсортовані шляхи
387
388
  */
388
- async function findK8sYamlFiles(root) {
389
+ async function findK8sYamlFiles(root, ignorePaths = []) {
389
390
  /** @type {string[]} */
390
391
  const out = []
391
- await walkDir(root, p => {
392
- if (!pathHasK8sSegment(p)) {
393
- return
394
- }
395
- if (!YAML_EXTENSION_RE.test(p)) {
396
- return
397
- }
398
- out.push(p)
399
- })
392
+ await walkDir(
393
+ root,
394
+ p => {
395
+ if (!pathHasK8sSegment(p)) {
396
+ return
397
+ }
398
+ if (!YAML_EXTENSION_RE.test(p)) {
399
+ return
400
+ }
401
+ out.push(p)
402
+ },
403
+ ignorePaths
404
+ )
400
405
  return [...out].toSorted((a, b) => a.localeCompare(b))
401
406
  }
402
407
 
@@ -2088,7 +2093,8 @@ export async function check() {
2088
2093
  await ensureNoFirebaseHostingArtifacts(root, pass, fail)
2089
2094
  await checkCleanMergedBranch(root, pass, fail)
2090
2095
 
2091
- const yamlFiles = await findK8sYamlFiles(root)
2096
+ const ignorePaths = await loadCursorIgnorePaths(root)
2097
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
2092
2098
  const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
2093
2099
 
2094
2100
  if (deploymentDirs.size > 0) {
@@ -33,6 +33,7 @@ import { basename } from 'node:path'
33
33
  import { getMirrorGcrHint, getFromImageToken } from './utils/docker-mirror.mjs'
34
34
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
35
35
  import { createCheckReporter } from './utils/check-reporter.mjs'
36
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
36
37
  import { walkDir } from './utils/walkDir.mjs'
37
38
 
38
39
  const NEWLINE_RE = /\r?\n/
@@ -65,14 +66,19 @@ export function isDockerfileName(name) {
65
66
  /**
66
67
  * Збирає абсолютні шляхи до Dockerfile / Containerfile від кореня cwd.
67
68
  * @param {string} root корінь репозиторію
69
+ * @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
68
70
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи
69
71
  */
70
- export async function findDockerfilePaths(root) {
72
+ export async function findDockerfilePaths(root, ignorePaths = []) {
71
73
  /** @type {string[]} */
72
74
  const out = []
73
- await walkDir(root, p => {
74
- if (isDockerfileName(basename(p))) out.push(p)
75
- })
75
+ await walkDir(
76
+ root,
77
+ p => {
78
+ if (isDockerfileName(basename(p))) out.push(p)
79
+ },
80
+ ignorePaths
81
+ )
76
82
  return out.toSorted((a, b) => a.localeCompare(b))
77
83
  }
78
84
 
@@ -285,7 +291,8 @@ export async function check() {
285
291
  const { pass } = reporter
286
292
 
287
293
  const root = process.cwd()
288
- const files = await findDockerfilePaths(root)
294
+ const ignorePaths = await loadCursorIgnorePaths(root)
295
+ const files = await findDockerfilePaths(root, ignorePaths)
289
296
 
290
297
  if (files.length === 0) {
291
298
  pass('Немає Dockerfile / Containerfile — перевірку hadolint пропущено')
@@ -17,6 +17,7 @@ import {
17
17
  shouldSkipFileForGqlScan,
18
18
  sourceFileHasGqlTaggedTemplate
19
19
  } from './utils/graphql-gql-scan.mjs'
20
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
20
21
  import { walkDir } from './utils/walkDir.mjs'
21
22
 
22
23
  /** Очікуваний файл GraphQL Config у корені (graphql.mdc). */
@@ -33,16 +34,20 @@ export const REQUIRED_DUMP_SCHEMA_SCRIPT =
33
34
  * @param {string} root абсолютний шлях кореня
34
35
  * @returns {Promise<string[]>} список кандидатів
35
36
  */
36
- async function collectScanCandidates(root) {
37
+ async function collectScanCandidates(root, ignorePaths) {
37
38
  /** @type {string[]} */
38
39
  const candidates = []
39
- await walkDir(root, absPath => {
40
- const rel = relative(root, absPath).split('\\').join('/')
41
- if (shouldSkipFileForGqlScan(rel) || !isGqlScanSourceFile(rel)) {
42
- return
43
- }
44
- candidates.push(absPath)
45
- })
40
+ await walkDir(
41
+ root,
42
+ absPath => {
43
+ const rel = relative(root, absPath).split('\\').join('/')
44
+ if (shouldSkipFileForGqlScan(rel) || !isGqlScanSourceFile(rel)) {
45
+ return
46
+ }
47
+ candidates.push(absPath)
48
+ },
49
+ ignorePaths
50
+ )
46
51
  return candidates
47
52
  }
48
53
 
@@ -148,7 +153,8 @@ export async function check() {
148
153
  const { pass, fail } = reporter
149
154
 
150
155
  const root = process.cwd()
151
- const candidates = await collectScanCandidates(root)
156
+ const ignorePaths = await loadCursorIgnorePaths(root)
157
+ const candidates = await collectScanCandidates(root, ignorePaths)
152
158
  const hits = await collectGqlHits(root, candidates)
153
159
 
154
160
  if (hits.length === 0) {
@@ -30,6 +30,7 @@ import { parseAllDocuments } from 'yaml'
30
30
 
31
31
  import { getRepositoryUrl } from './auto-rules.mjs'
32
32
  import { createCheckReporter } from './utils/check-reporter.mjs'
33
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
33
34
  import { walkDir } from './utils/walkDir.mjs'
34
35
 
35
36
  const NITRA_REPOSITORY_URL_MARKER = 'https://github.com/nitra/'
@@ -103,15 +104,19 @@ export function isEnvFile(relPath) {
103
104
  * @param {string} root абсолютний шлях кореня
104
105
  * @returns {Promise<string[]>} відсортовані posix-шляхи відносно кореня
105
106
  */
106
- async function collectEnvFiles(root) {
107
+ async function collectEnvFiles(root, ignorePaths) {
107
108
  /** @type {string[]} */
108
109
  const out = []
109
- await walkDir(root, absPath => {
110
- const rel = relative(root, absPath).split('\\').join('/')
111
- if (isEnvFile(rel)) {
112
- out.push(rel)
113
- }
114
- })
110
+ await walkDir(
111
+ root,
112
+ absPath => {
113
+ const rel = relative(root, absPath).split('\\').join('/')
114
+ if (isEnvFile(rel)) {
115
+ out.push(rel)
116
+ }
117
+ },
118
+ ignorePaths
119
+ )
115
120
  return out.toSorted((a, b) => a.localeCompare(b))
116
121
  }
117
122
 
@@ -206,7 +211,8 @@ export async function check() {
206
211
  namespace: await readYamlMetadataName(join(root, HASURA_NAMESPACE_FILE), 'Namespace')
207
212
  }
208
213
 
209
- const envFiles = await collectEnvFiles(root)
214
+ const ignorePaths = await loadCursorIgnorePaths(root)
215
+ const envFiles = await collectEnvFiles(root, ignorePaths)
210
216
  if (envFiles.length === 0) {
211
217
  pass('Не знайдено жодного *.env файла — нічого перевіряти')
212
218
  return reporter.getExitCode()
@@ -25,6 +25,7 @@ import { readFile } from 'node:fs/promises'
25
25
  import { join, relative } from 'node:path'
26
26
 
27
27
  import { createCheckReporter } from './utils/check-reporter.mjs'
28
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
28
29
  import { walkDir } from './utils/walkDir.mjs'
29
30
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
30
31
 
@@ -216,16 +217,20 @@ function packageHasAvifDisabled(pkg) {
216
217
  * @param {(msg: string) => void} fail callback при помилці
217
218
  * @returns {Promise<void>}
218
219
  */
219
- async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, pass, fail) {
220
+ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, pass, fail) {
220
221
  const absRoot = join(process.cwd(), packageRoot)
221
222
  const label = packageRoot === '.' ? 'корінь' : packageRoot
222
223
  /** @type {string[]} */
223
224
  const vueFiles = []
224
- await walkDir(absRoot, absPath => {
225
- if (!absPath.endsWith('.vue')) return
226
- if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
227
- vueFiles.push(absPath)
228
- })
225
+ await walkDir(
226
+ absRoot,
227
+ absPath => {
228
+ if (!absPath.endsWith('.vue')) return
229
+ if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
230
+ vueFiles.push(absPath)
231
+ },
232
+ ignorePaths
233
+ )
229
234
  if (vueFiles.length === 0) return
230
235
 
231
236
  let violations = 0
@@ -262,7 +267,7 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, pass, fa
262
267
  * @param {(msg: string) => void} fail callback при помилці
263
268
  * @returns {Promise<void>}
264
269
  */
265
- async function checkVueAvifImports(pass, fail) {
270
+ async function checkVueAvifImports(ignorePaths, pass, fail) {
266
271
  const roots = await getMonorepoPackageRootDirs()
267
272
  const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
268
273
  for (const root of roots) {
@@ -274,7 +279,7 @@ async function checkVueAvifImports(pass, fail) {
274
279
  continue
275
280
  }
276
281
  const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
277
- await checkVueAvifImportsInPackage(root, otherRootsAbs, pass, fail)
282
+ await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, pass, fail)
278
283
  }
279
284
  }
280
285
 
@@ -314,7 +319,8 @@ export async function check() {
314
319
  await checkHashCacheNotIgnored(pass, fail)
315
320
  await checkLegacyCacheRemoved(pass, fail)
316
321
  }
317
- await checkVueAvifImports(pass, fail)
322
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
323
+ await checkVueAvifImports(ignorePaths, pass, fail)
318
324
 
319
325
  return reporter.getExitCode()
320
326
  }
@@ -35,6 +35,7 @@ import {
35
35
  isBunSqlScanSourceFile,
36
36
  textHasBunSqlImport
37
37
  } from './utils/bun-sql-scan.mjs'
38
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
38
39
  import { walkDir } from './utils/walkDir.mjs'
39
40
 
40
41
  /** Імена забороненої залежності у будь-якому `package.json`. */
@@ -54,14 +55,18 @@ function asObject(v) {
54
55
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
55
56
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
56
57
  */
57
- async function findAllPackageJsonPaths(repoRoot) {
58
+ async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
58
59
  /** @type {string[]} */
59
60
  const paths = []
60
- await walkDir(repoRoot, absPath => {
61
- if (absPath.endsWith(`${sep}package.json`)) {
62
- paths.push(absPath)
63
- }
64
- })
61
+ await walkDir(
62
+ repoRoot,
63
+ absPath => {
64
+ if (absPath.endsWith(`${sep}package.json`)) {
65
+ paths.push(absPath)
66
+ }
67
+ },
68
+ ignorePaths
69
+ )
65
70
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
66
71
  return paths
67
72
  }
@@ -71,15 +76,19 @@ async function findAllPackageJsonPaths(repoRoot) {
71
76
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
72
77
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
73
78
  */
74
- async function findAllSourcePathsForBunSqlScan(repoRoot) {
79
+ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
75
80
  /** @type {string[]} */
76
81
  const paths = []
77
- await walkDir(repoRoot, absPath => {
78
- const rel = relative(repoRoot, absPath).split('\\').join('/')
79
- if (isBunSqlScanSourceFile(rel)) {
80
- paths.push(absPath)
81
- }
82
- })
82
+ await walkDir(
83
+ repoRoot,
84
+ absPath => {
85
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
86
+ if (isBunSqlScanSourceFile(rel)) {
87
+ paths.push(absPath)
88
+ }
89
+ },
90
+ ignorePaths
91
+ )
83
92
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
84
93
  return paths
85
94
  }
@@ -236,7 +245,8 @@ export async function check() {
236
245
  return reporter.getExitCode()
237
246
  }
238
247
 
239
- const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot)
248
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
249
+ const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
240
250
  if (pkgJsonPaths.length === 0) {
241
251
  pass('js-bun-db: package.json не знайдено — перевірку пропущено')
242
252
  return reporter.getExitCode()
@@ -244,7 +254,7 @@ export async function check() {
244
254
 
245
255
  await checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter)
246
256
 
247
- const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot)
257
+ const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
248
258
  if (sourcePaths.length === 0) {
249
259
  pass('js-bun-db: немає JS/TS файлів для скану патернів Bun SQL')
250
260
  return reporter.getExitCode()
@@ -22,6 +22,7 @@ import {
22
22
  findUnsafeMssqlInListMissingEmptyGuardInText,
23
23
  isMssqlScanSourceFile
24
24
  } from './utils/mssql-pool-scan.mjs'
25
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
25
26
  import { walkDir } from './utils/walkDir.mjs'
26
27
 
27
28
  const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/u
@@ -35,14 +36,18 @@ const MIN_MSSQL_VERSION = { major: 12, minor: 5, patch: 0 }
35
36
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
36
37
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
37
38
  */
38
- async function findAllPackageJsonPaths(repoRoot) {
39
+ async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
39
40
  /** @type {string[]} */
40
41
  const paths = []
41
- await walkDir(repoRoot, absPath => {
42
- if (absPath.endsWith(`${sep}package.json`)) {
43
- paths.push(absPath)
44
- }
45
- })
42
+ await walkDir(
43
+ repoRoot,
44
+ absPath => {
45
+ if (absPath.endsWith(`${sep}package.json`)) {
46
+ paths.push(absPath)
47
+ }
48
+ },
49
+ ignorePaths
50
+ )
46
51
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
47
52
  return paths
48
53
  }
@@ -52,15 +57,19 @@ async function findAllPackageJsonPaths(repoRoot) {
52
57
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
53
58
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
54
59
  */
55
- async function findAllSourcePathsForMssqlScan(repoRoot) {
60
+ async function findAllSourcePathsForMssqlScan(repoRoot, ignorePaths) {
56
61
  /** @type {string[]} */
57
62
  const paths = []
58
- await walkDir(repoRoot, absPath => {
59
- const rel = relative(repoRoot, absPath).split('\\').join('/')
60
- if (isMssqlScanSourceFile(rel)) {
61
- paths.push(absPath)
62
- }
63
- })
63
+ await walkDir(
64
+ repoRoot,
65
+ absPath => {
66
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
67
+ if (isMssqlScanSourceFile(rel)) {
68
+ paths.push(absPath)
69
+ }
70
+ },
71
+ ignorePaths
72
+ )
64
73
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
65
74
  return paths
66
75
  }
@@ -253,8 +262,8 @@ function reportZeroMssqlSourceViolations(counters, pass) {
253
262
  * @param {(msg: string) => void} fail fail callback
254
263
  * @returns {Promise<void>}
255
264
  */
256
- async function auditMssqlSources(repoRoot, pass, fail) {
257
- const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot)
265
+ async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
266
+ const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)
258
267
  if (sourcePaths.length === 0) {
259
268
  pass('js-mssql: немає JS/TS файлів для скану singleton ConnectionPool')
260
269
  return
@@ -291,7 +300,8 @@ export async function check() {
291
300
  return reporter.getExitCode()
292
301
  }
293
302
 
294
- const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot)
303
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
304
+ const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
295
305
  if (pkgJsonPaths.length === 0) {
296
306
  pass('js-mssql: package.json не знайдено — перевірку пропущено')
297
307
  return reporter.getExitCode()
@@ -307,7 +317,7 @@ export async function check() {
307
317
  pass(`js-mssql: всі знайдені dependencies.mssql відповідають мінімальній версії 12.5.0 (${found})`)
308
318
  }
309
319
 
310
- await auditMssqlSources(repoRoot, pass, fail)
320
+ await auditMssqlSources(repoRoot, ignorePaths, pass, fail)
311
321
 
312
322
  return reporter.getExitCode()
313
323
  }
@@ -36,6 +36,7 @@ import {
36
36
  isInsideConnDir,
37
37
  resolveConnDirFromPackageJson
38
38
  } from './utils/conn-imports-scan.mjs'
39
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
39
40
  import { walkDir } from './utils/walkDir.mjs'
40
41
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
41
42
 
@@ -56,15 +57,19 @@ function relPosix(absPackageRoot, absPath) {
56
57
  * @param {(msg: string) => void} fail callback при помилці
57
58
  * @returns {Promise<number>} кількість знайдених порушень
58
59
  */
59
- async function checkBunyanImports(absPackageRoot, label, fail) {
60
+ async function checkBunyanImports(absPackageRoot, ignorePaths, label, fail) {
60
61
  /** @type {string[]} */
61
62
  const sourcePaths = []
62
- await walkDir(absPackageRoot, absPath => {
63
- const rel = relPosix(absPackageRoot, absPath)
64
- if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
65
- sourcePaths.push(absPath)
66
- }
67
- })
63
+ await walkDir(
64
+ absPackageRoot,
65
+ absPath => {
66
+ const rel = relPosix(absPackageRoot, absPath)
67
+ if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
68
+ sourcePaths.push(absPath)
69
+ }
70
+ },
71
+ ignorePaths
72
+ )
68
73
 
69
74
  let violations = 0
70
75
  for (const absPath of sourcePaths) {
@@ -83,13 +88,17 @@ async function checkBunyanImports(absPackageRoot, label, fail) {
83
88
  * @param {string} absPackageRoot абсолютний шлях до кореня пакета
84
89
  * @returns {Promise<string[]>} абсолютні шляхи до файлів
85
90
  */
86
- async function collectSourceFiles(absPackageRoot) {
91
+ async function collectSourceFiles(absPackageRoot, ignorePaths) {
87
92
  /** @type {string[]} */
88
93
  const out = []
89
- await walkDir(absPackageRoot, absPath => {
90
- const rel = relPosix(absPackageRoot, absPath)
91
- if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
92
- })
94
+ await walkDir(
95
+ absPackageRoot,
96
+ absPath => {
97
+ const rel = relPosix(absPackageRoot, absPath)
98
+ if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
99
+ },
100
+ ignorePaths
101
+ )
93
102
  return out
94
103
  }
95
104
 
@@ -153,17 +162,17 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
153
162
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
154
163
  * @returns {Promise<void>} завершується після перевірок цього пакета
155
164
  */
156
- async function checkWorkspacePackage(rootDir, fail, passFn) {
165
+ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
157
166
  const label = `[${rootDir}] `
158
167
  const absPackageRoot = join(process.cwd(), rootDir)
159
168
  const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
160
169
 
161
- const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
170
+ const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
162
171
  if (importViolations === 0) {
163
172
  passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
164
173
  }
165
174
 
166
- const sourcePaths = await collectSourceFiles(absPackageRoot)
175
+ const sourcePaths = await collectSourceFiles(absPackageRoot, ignorePaths)
167
176
 
168
177
  const connViolations = await checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail)
169
178
  if (connViolations === 0) {
@@ -245,8 +254,9 @@ export async function check() {
245
254
  return reporter.getExitCode()
246
255
  }
247
256
 
257
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
248
258
  for (const r of workspaceRoots) {
249
- await checkWorkspacePackage(r, fail, pass)
259
+ await checkWorkspacePackage(r, ignorePaths, fail, pass)
250
260
  }
251
261
 
252
262
  return reporter.getExitCode()
@@ -109,6 +109,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
109
109
  import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
110
110
 
111
111
  import { createCheckReporter } from './utils/check-reporter.mjs'
112
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
112
113
  import { walkDir } from './utils/walkDir.mjs'
113
114
 
114
115
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -1581,16 +1582,21 @@ export function baseKustomizationNamespaceViolation(obj) {
1581
1582
  /**
1582
1583
  * Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
1583
1584
  * @param {string} root корінь репозиторію (cwd)
1585
+ * @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
1584
1586
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
1585
1587
  */
1586
- async function findK8sYamlFiles(root) {
1588
+ async function findK8sYamlFiles(root, ignorePaths = []) {
1587
1589
  /** @type {string[]} */
1588
1590
  const out = []
1589
- await walkDir(root, p => {
1590
- if (!pathHasK8sSegment(p)) return
1591
- if (!YAML_EXTENSION_RE.test(p)) return
1592
- out.push(p)
1593
- })
1591
+ await walkDir(
1592
+ root,
1593
+ p => {
1594
+ if (!pathHasK8sSegment(p)) return
1595
+ if (!YAML_EXTENSION_RE.test(p)) return
1596
+ out.push(p)
1597
+ },
1598
+ ignorePaths
1599
+ )
1594
1600
 
1595
1601
  return out.toSorted((a, b) => a.localeCompare(b))
1596
1602
  }
@@ -1659,12 +1665,13 @@ export function classifyBackendConfigManifestPresence(body) {
1659
1665
  /**
1660
1666
  * Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
1661
1667
  * @param {string} root корінь репозиторію
1668
+ * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1662
1669
  * @param {(msg: string) => void} fail реєстрація порушення
1663
1670
  * @param {(msg: string) => void} pass реєстрація успіху
1664
1671
  * @returns {Promise<void>}
1665
1672
  */
1666
- async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
1667
- const yamlFiles = await findK8sYamlFiles(root)
1673
+ async function removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass) {
1674
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
1668
1675
  for (const abs of yamlFiles) {
1669
1676
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
1670
1677
  try {
@@ -1736,12 +1743,13 @@ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
1736
1743
  /**
1737
1744
  * Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
1738
1745
  * @param {string} root корінь репозиторію
1746
+ * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1739
1747
  * @param {(msg: string) => void} fail колбек повідомлення про помилку
1740
1748
  * @param {(msg: string) => void} pass колбек успішного повідомлення
1741
1749
  * @returns {Promise<void>}
1742
1750
  */
1743
- async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
1744
- const yamlFiles = await findK8sYamlFiles(root)
1751
+ async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass) {
1752
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
1745
1753
  for (const abs of yamlFiles) {
1746
1754
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
1747
1755
  try {
@@ -5762,12 +5770,13 @@ export async function check() {
5762
5770
  const { pass, fail } = reporter
5763
5771
 
5764
5772
  const root = process.cwd()
5773
+ const ignorePaths = await loadCursorIgnorePaths(root)
5765
5774
 
5766
- await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass)
5775
+ await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass)
5767
5776
 
5768
- await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
5777
+ await removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass)
5769
5778
 
5770
- const yamlFiles = await findK8sYamlFiles(root)
5779
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
5771
5780
 
5772
5781
  if (yamlFiles.length === 0) {
5773
5782
  pass('Немає *.yaml під k8s — перевірку $schema пропущено')
@@ -19,6 +19,7 @@ import { basename, dirname, join, relative } from 'node:path'
19
19
 
20
20
  import { findDockerfilePaths } from './check-docker.mjs'
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
22
23
  import { walkDir } from './utils/walkDir.mjs'
23
24
 
24
25
  const LINE_SPLIT_RE = /\r?\n/u
@@ -36,15 +37,19 @@ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
36
37
  * @param {string} root корінь cwd
37
38
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
38
39
  */
39
- export async function findDefaultConfTemplatePaths(root) {
40
+ export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
40
41
  /** @type {string[]} */
41
42
  const out = []
42
- await walkDir(root, p => {
43
- if (basename(p) !== 'default.conf.template') return
44
- const rel = relative(root, p).replaceAll('\\', '/')
45
- if (rel.includes('tests/fixtures/')) return
46
- out.push(p)
47
- })
43
+ await walkDir(
44
+ root,
45
+ p => {
46
+ if (basename(p) !== 'default.conf.template') return
47
+ const rel = relative(root, p).replaceAll('\\', '/')
48
+ if (rel.includes('tests/fixtures/')) return
49
+ out.push(p)
50
+ },
51
+ ignorePaths
52
+ )
48
53
  return out.toSorted((a, b) => a.localeCompare(b))
49
54
  }
50
55
 
@@ -54,12 +59,16 @@ export async function findDefaultConfTemplatePaths(root) {
54
59
  * @param {string} root корінь обходу (зазвичай cwd репозиторію)
55
60
  * @returns {Promise<{ renamed: string[], overwritten: string[] }>} відносні шляхи до обробленого **default.tpl.conf** (для звіту)
56
61
  */
57
- export async function migrateDefaultTplConfFiles(root) {
62
+ export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
58
63
  /** @type {string[]} */
59
64
  const oldPaths = []
60
- await walkDir(root, p => {
61
- if (basename(p) === 'default.tpl.conf') oldPaths.push(p)
62
- })
65
+ await walkDir(
66
+ root,
67
+ p => {
68
+ if (basename(p) === 'default.tpl.conf') oldPaths.push(p)
69
+ },
70
+ ignorePaths
71
+ )
63
72
  oldPaths.sort((a, b) => a.localeCompare(b))
64
73
 
65
74
  /** @type {string[]} */
@@ -316,8 +325,8 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
316
325
  * @param {(msg: string) => void} passFn callback при успішній перевірці
317
326
  * @param {(msg: string) => void} failFn callback при помилці
318
327
  */
319
- async function checkDockerfiles(root, passFn, failFn) {
320
- const dockerPaths = await findDockerfilePaths(root)
328
+ async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
329
+ const dockerPaths = await findDockerfilePaths(root, ignorePaths)
321
330
  if (dockerPaths.length === 0) {
322
331
  failFn(
323
332
  'Є default.conf.template, але немає Dockerfile / Containerfile — додай gzip для статики та envsubst (див. nginx-default-tpl.mdc)'
@@ -380,8 +389,9 @@ export async function check() {
380
389
  const { pass, fail } = reporter
381
390
 
382
391
  const root = process.cwd()
392
+ const ignorePaths = await loadCursorIgnorePaths(root)
383
393
 
384
- const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root)
394
+ const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root, ignorePaths)
385
395
  for (const rel of tplRenamed) {
386
396
  pass(`Перейменовано default.tpl.conf → default.conf.template: ${rel}`)
387
397
  }
@@ -389,7 +399,7 @@ export async function check() {
389
399
  pass(`Перезаписано default.conf.template змістом default.tpl.conf: ${rel}`)
390
400
  }
391
401
 
392
- const templates = await findDefaultConfTemplatePaths(root)
402
+ const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
393
403
 
394
404
  if (templates.length === 0) {
395
405
  pass('Немає default.conf.template — перевірку nginx-default-tpl пропущено')
@@ -402,7 +412,7 @@ export async function check() {
402
412
  await checkTemplateFile(abs, root, pass, fail)
403
413
  }
404
414
 
405
- await checkDockerfiles(root, pass, fail)
415
+ await checkDockerfiles(root, ignorePaths, pass, fail)
406
416
  await checkVscodeNginx(pass, fail)
407
417
 
408
418
  return reporter.getExitCode()
@@ -26,6 +26,7 @@ import {
26
26
  pushHasMainBranch,
27
27
  pushPathsIncludeNpmGlob
28
28
  } from './utils/gha-workflow.mjs'
29
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
29
30
  import { walkDir } from './utils/walkDir.mjs'
30
31
 
31
32
  const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
@@ -43,17 +44,21 @@ const CHANGELOG_PATH = 'npm/CHANGELOG.md'
43
44
  * Чи є під `npm/src` хоча б один `.js` (рекурсивно).
44
45
  * @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
45
46
  */
46
- async function npmSrcTreeHasJsFile() {
47
+ async function npmSrcTreeHasJsFile(ignorePaths = []) {
47
48
  const root = 'npm/src'
48
49
  if (!existsSync(root)) {
49
50
  return false
50
51
  }
51
52
  let found = false
52
- await walkDir(root, p => {
53
- if (p.endsWith('.js')) {
54
- found = true
55
- }
56
- })
53
+ await walkDir(
54
+ root,
55
+ p => {
56
+ if (p.endsWith('.js')) {
57
+ found = true
58
+ }
59
+ },
60
+ ignorePaths
61
+ )
57
62
  return found
58
63
  }
59
64
 
@@ -387,7 +392,8 @@ export async function check() {
387
392
 
388
393
  await checkNpmModuleBasicStructure(pass, fail)
389
394
 
390
- const useSrcJsLayout = await npmSrcTreeHasJsFile()
395
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
396
+ const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
391
397
 
392
398
  await checkNpmPackageJson(useSrcJsLayout, pass, fail)
393
399
 
@@ -20,6 +20,7 @@ import {
20
20
  isVueImportScanSourceFile,
21
21
  shouldSkipFileForVueImportScan
22
22
  } from './utils/vue-forbidden-imports.mjs'
23
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
23
24
  import { walkDir } from './utils/walkDir.mjs'
24
25
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
25
26
 
@@ -113,14 +114,18 @@ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
113
114
  * @param {(msg: string) => void} passFn callback при успішній перевірці
114
115
  * @param {(msg: string) => void} fail callback при помилці
115
116
  */
116
- async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fail) {
117
+ async function checkEsbuildMentions(rootDir, absPackageRoot, ignorePaths, prefix, passFn, fail) {
117
118
  /** @type {{ rel: string }[]} */
118
119
  const candidates = []
119
- await walkDir(absPackageRoot, absPath => {
120
- const rel = relative(absPackageRoot, absPath).split('\\').join('/')
121
- if (!isEsbuildScanFile(rel)) return
122
- candidates.push({ rel })
123
- })
120
+ await walkDir(
121
+ absPackageRoot,
122
+ absPath => {
123
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
124
+ if (!isEsbuildScanFile(rel)) return
125
+ candidates.push({ rel })
126
+ },
127
+ ignorePaths
128
+ )
124
129
 
125
130
  const maxMatches = 30
126
131
  const matches = await collectEsbuildMatchesInFiles(absPackageRoot, candidates, maxMatches)
@@ -308,7 +313,7 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
308
313
  * @param {(msg: string) => void} passFn callback при успішній перевірці
309
314
  * @param {(msg: string) => void} fail callback при помилці
310
315
  */
311
- async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImport, prefix, passFn, fail) {
316
+ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, hasVueAutoImport, prefix, passFn, fail) {
312
317
  if (!hasVueAutoImport) {
313
318
  passFn(
314
319
  `${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`
@@ -317,12 +322,16 @@ async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImpor
317
322
  }
318
323
  /** @type {string[]} */
319
324
  const sourcePaths = []
320
- await walkDir(absPackageRoot, absPath => {
321
- const rel = relative(absPackageRoot, absPath).split('\\').join('/')
322
- if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
323
- sourcePaths.push(absPath)
324
- }
325
- })
325
+ await walkDir(
326
+ absPackageRoot,
327
+ absPath => {
328
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
329
+ if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
330
+ sourcePaths.push(absPath)
331
+ }
332
+ },
333
+ ignorePaths
334
+ )
326
335
 
327
336
  let importViolations = 0
328
337
  for (const absPath of sourcePaths) {
@@ -347,7 +356,7 @@ async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImpor
347
356
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
348
357
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
349
358
  */
350
- async function checkVuePackage(rootDir, fail, passFn) {
359
+ async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
351
360
  const prefix = `[${packageLabel(rootDir)}] `
352
361
  const pkg = JSON.parse(await readFile(join(rootDir, 'package.json'), 'utf8'))
353
362
  const deps = pkg.dependencies || {}
@@ -387,8 +396,8 @@ async function checkVuePackage(rootDir, fail, passFn) {
387
396
  )
388
397
 
389
398
  const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
390
- await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), hasVueAutoImport, prefix, passFn, fail)
391
- await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
399
+ await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, hasVueAutoImport, prefix, passFn, fail)
400
+ await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
392
401
  }
393
402
 
394
403
  /**
@@ -446,8 +455,9 @@ export async function check() {
446
455
 
447
456
  await checkVueVolarRecommendation(pass, fail)
448
457
 
458
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
449
459
  for (const r of vueRoots) {
450
- await checkVuePackage(r, fail, pass)
460
+ await checkVuePackage(r, ignorePaths, fail, pass)
451
461
  }
452
462
 
453
463
  return reporter.getExitCode()
@@ -14,6 +14,7 @@ import { rename } from 'node:fs/promises'
14
14
  import { cwd } from 'node:process'
15
15
  import { relative, resolve } from 'node:path'
16
16
 
17
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
17
18
  import { walkDir } from './utils/walkDir.mjs'
18
19
 
19
20
  const K8S_YML_RE = /\.yml$/iu
@@ -69,37 +70,41 @@ export function replaceExtension(relPosix, newExt) {
69
70
  * @param {string} rootAbs абсолютний корінь репозиторію
70
71
  * @returns {Promise<Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>>} відсортовані операції перейменування без запису на диск
71
72
  */
72
- async function collectRenameOps(rootAbs) {
73
+ async function collectRenameOps(rootAbs, ignorePaths) {
73
74
  /** @type {Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>} */
74
75
  const ops = []
75
76
 
76
- await walkDir(rootAbs, fileAbs => {
77
- const rel = posixRelFromRoot(rootAbs, fileAbs)
78
- if (rel === null) return
79
- if (pathMatchesK8sYml(rel)) {
80
- const relTo = replaceExtension(rel, '.yaml')
81
- if (relTo === rel) return
82
- ops.push({
83
- kind: 'k8s',
84
- fromAbs: resolve(rootAbs, rel),
85
- toAbs: resolve(rootAbs, relTo),
86
- relFrom: rel,
87
- relTo
88
- })
89
- return
90
- }
91
- if (pathMatchesGithubYaml(rel)) {
92
- const relTo = replaceExtension(rel, '.yml')
93
- if (relTo === rel) return
94
- ops.push({
95
- kind: 'github',
96
- fromAbs: resolve(rootAbs, rel),
97
- toAbs: resolve(rootAbs, relTo),
98
- relFrom: rel,
99
- relTo
100
- })
101
- }
102
- })
77
+ await walkDir(
78
+ rootAbs,
79
+ fileAbs => {
80
+ const rel = posixRelFromRoot(rootAbs, fileAbs)
81
+ if (rel === null) return
82
+ if (pathMatchesK8sYml(rel)) {
83
+ const relTo = replaceExtension(rel, '.yaml')
84
+ if (relTo === rel) return
85
+ ops.push({
86
+ kind: 'k8s',
87
+ fromAbs: resolve(rootAbs, rel),
88
+ toAbs: resolve(rootAbs, relTo),
89
+ relFrom: rel,
90
+ relTo
91
+ })
92
+ return
93
+ }
94
+ if (pathMatchesGithubYaml(rel)) {
95
+ const relTo = replaceExtension(rel, '.yml')
96
+ if (relTo === rel) return
97
+ ops.push({
98
+ kind: 'github',
99
+ fromAbs: resolve(rootAbs, rel),
100
+ toAbs: resolve(rootAbs, relTo),
101
+ relFrom: rel,
102
+ relTo
103
+ })
104
+ }
105
+ },
106
+ ignorePaths
107
+ )
103
108
 
104
109
  ops.sort((a, b) => {
105
110
  const ko = (a.kind === 'k8s' ? 0 : 1) - (b.kind === 'k8s' ? 0 : 1)
@@ -119,7 +124,8 @@ async function collectRenameOps(rootAbs) {
119
124
  export async function renameYamlExtensions(root, options = {}) {
120
125
  const dryRun = options.dryRun === true
121
126
  const rootAbs = resolve(root)
122
- const ops = await collectRenameOps(rootAbs)
127
+ const ignorePaths = await loadCursorIgnorePaths(rootAbs)
128
+ const ops = await collectRenameOps(rootAbs, ignorePaths)
123
129
 
124
130
  /** @type { { relFrom: string, relTo: string }[]} */
125
131
  const renamed = []
@@ -12,6 +12,7 @@ import { basename } from 'node:path'
12
12
  import { isRunAsCli } from './cli-entry.mjs'
13
13
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
14
14
  import { createCheckReporter } from './utils/check-reporter.mjs'
15
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
15
16
  import { walkDir } from './utils/walkDir.mjs'
16
17
 
17
18
  /**
@@ -30,12 +31,16 @@ export function isLintDockerfileName(name) {
30
31
  * @param {string} root корінь репозиторію
31
32
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи
32
33
  */
33
- export async function findLintDockerfilePaths(root) {
34
+ export async function findLintDockerfilePaths(root, ignorePaths = []) {
34
35
  /** @type {string[]} */
35
36
  const out = []
36
- await walkDir(root, p => {
37
- if (isLintDockerfileName(basename(p))) out.push(p)
38
- })
37
+ await walkDir(
38
+ root,
39
+ p => {
40
+ if (isLintDockerfileName(basename(p))) out.push(p)
41
+ },
42
+ ignorePaths
43
+ )
39
44
  return out.toSorted((a, b) => a.localeCompare(b))
40
45
  }
41
46
 
@@ -48,7 +53,8 @@ async function main() {
48
53
  const { pass, fail } = reporter
49
54
 
50
55
  const root = process.cwd()
51
- const files = await findLintDockerfilePaths(root)
56
+ const ignorePaths = await loadCursorIgnorePaths(root)
57
+ const files = await findLintDockerfilePaths(root, ignorePaths)
52
58
 
53
59
  if (files.length === 0) {
54
60
  pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')
@@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process'
16
16
  import { basename, dirname } from 'node:path'
17
17
 
18
18
  import { isRunAsCli } from './cli-entry.mjs'
19
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
19
20
  import { resolveCmd } from './utils/resolve-cmd.mjs'
20
21
  import { walkDir } from './utils/walkDir.mjs'
21
22
 
@@ -60,15 +61,19 @@ export function k8sRootFromFile(absFile) {
60
61
  * @param {string} root корінь репозиторію
61
62
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
62
63
  */
63
- export async function findK8sRoots(root) {
64
+ export async function findK8sRoots(root, ignorePaths = []) {
64
65
  /** @type {Set<string>} */
65
66
  const roots = new Set()
66
- await walkDir(root, p => {
67
- if (!pathHasK8sSegment(p)) return
68
- if (!YAML_EXT_RE.test(p)) return
69
- const k8sRoot = k8sRootFromFile(p)
70
- if (k8sRoot) roots.add(k8sRoot)
71
- })
67
+ await walkDir(
68
+ root,
69
+ p => {
70
+ if (!pathHasK8sSegment(p)) return
71
+ if (!YAML_EXT_RE.test(p)) return
72
+ const k8sRoot = k8sRootFromFile(p)
73
+ if (k8sRoot) roots.add(k8sRoot)
74
+ },
75
+ ignorePaths
76
+ )
72
77
  return [...roots].toSorted((a, b) => a.localeCompare(b))
73
78
  }
74
79
 
@@ -134,7 +139,8 @@ function runKubescape(dirs) {
134
139
  */
135
140
  async function main() {
136
141
  const root = process.cwd()
137
- const dirs = await findK8sRoots(root)
142
+ const ignorePaths = await loadCursorIgnorePaths(root)
143
+ const dirs = await findK8sRoots(root, ignorePaths)
138
144
 
139
145
  if (dirs.length === 0) {
140
146
  console.log('run-k8s: немає *.yaml під k8s — kubeconform і kubescape пропущено')
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Утиліта читання `.n-cursor.json` у корені репозиторію.
3
+ *
4
+ * Зараз експортує `loadCursorIgnorePaths(root)` — список абсолютних posix-шляхів каталогів,
5
+ * які check-скрипти повністю виключають з обходу (поле `ignore` у конфізі).
6
+ */
7
+ import { existsSync } from 'node:fs'
8
+ import { readFile } from 'node:fs/promises'
9
+ import { isAbsolute, join, resolve, sep } from 'node:path'
10
+
11
+ const CONFIG_FILE = '.n-cursor.json'
12
+
13
+ /**
14
+ * Нормалізує шлях до абсолютного posix-формату без trailing-slash.
15
+ * Відносні шляхи розв'язуються від `root`.
16
+ * @param {string} root абсолютний корінь репозиторію
17
+ * @param {string} p шлях з конфігу (відносний або абсолютний)
18
+ * @returns {string} абсолютний posix-шлях
19
+ */
20
+ function toAbsPosix(root, p) {
21
+ const trimmed = String(p).trim()
22
+ const abs = isAbsolute(trimmed) ? trimmed : resolve(root, trimmed)
23
+ return abs.split(sep).join('/').replace(/\/+$/, '')
24
+ }
25
+
26
+ /**
27
+ * Читає `.n-cursor.json` з кореня та повертає нормалізовані ignore-шляхи.
28
+ * Якщо файлу нема, поле `ignore` відсутнє чи має невалідний формат — повертає порожній масив.
29
+ * Сам конфіг не валідується (це робить v8r/окрема перевірка) — лише поле `ignore`.
30
+ * @param {string} root абсолютний корінь репозиторію
31
+ * @returns {Promise<string[]>} абсолютні posix-шляхи без trailing-slash
32
+ */
33
+ export async function loadCursorIgnorePaths(root) {
34
+ const file = join(root, CONFIG_FILE)
35
+ if (!existsSync(file)) return []
36
+ let raw
37
+ try {
38
+ raw = JSON.parse(await readFile(file, 'utf8'))
39
+ } catch {
40
+ return []
41
+ }
42
+ const list = raw?.ignore
43
+ if (!Array.isArray(list)) return []
44
+ /** @type {string[]} */
45
+ const out = []
46
+ for (const item of list) {
47
+ if (typeof item !== 'string') continue
48
+ const v = item.trim()
49
+ if (v.length === 0) continue
50
+ out.push(toAbsPosix(root, v))
51
+ }
52
+ return out
53
+ }
@@ -2,19 +2,60 @@
2
2
  * Рекурсивний обхід каталогів для скриптів перевірки (Dockerfile, k8s YAML тощо).
3
3
  *
4
4
  * Обходить дерево від заданого кореня; для кожного звичайного файлу викликає переданий callback.
5
- * Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться. Якщо readdir для
6
- * каталогу не вдаєтьсятихо виходить без throw.
5
+ * Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться.
6
+ * Додатково можна передати `ignorePaths` повні шляхи каталогів (абсолютні posix), які слід
7
+ * пропускати разом з усім вмістом (поле `ignore` у `.n-cursor.json`). Якщо readdir для каталогу
8
+ * не вдається — тихо виходить без throw.
7
9
  */
8
10
  import { readdir } from 'node:fs/promises'
9
- import { join } from 'node:path'
11
+ import { isAbsolute, join, resolve, sep } from 'node:path'
10
12
 
11
13
  /**
12
- * Рекурсивно обходить каталог, пропускає типові артефакти збірки та залежностей.
14
+ * Перетворює довільний шлях у абсолютний posix-формат без trailing-slash.
15
+ * @param {string} p шлях
16
+ * @returns {string} абсолютний posix-шлях
17
+ */
18
+ function toAbsPosix(p) {
19
+ const abs = isAbsolute(p) ? p : resolve(p)
20
+ return abs.split(sep).join('/').replace(/\/+$/, '')
21
+ }
22
+
23
+ /**
24
+ * Чи каталог `dirAbsPosix` входить у список ignore (точний збіг або префікс з '/').
25
+ * Часткові збіги басенейму не враховуються (postgres-master-test ≠ postgres-master).
26
+ * @param {string} dirAbsPosix абсолютний posix-шлях каталогу
27
+ * @param {string[]} ignorePosix вже нормалізовані ignore-шляхи
28
+ * @returns {boolean}
29
+ */
30
+ function isIgnoredDir(dirAbsPosix, ignorePosix) {
31
+ for (const ig of ignorePosix) {
32
+ if (dirAbsPosix === ig) return true
33
+ if (dirAbsPosix.startsWith(`${ig}/`)) return true
34
+ }
35
+ return false
36
+ }
37
+
38
+ /**
39
+ * Рекурсивно обходить каталог, пропускає типові артефакти збірки/залежностей та `ignorePaths`.
13
40
  * @param {string} dir абсолютний шлях
14
41
  * @param {(filePath: string) => void} onFile виклик для кожного файлу
42
+ * @param {string[]} [ignorePaths=[]] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
43
+ * @returns {Promise<void>}
44
+ */
45
+ export async function walkDir(dir, onFile, ignorePaths = []) {
46
+ const ignorePosix = ignorePaths.map(toAbsPosix)
47
+ await walkDirInner(dir, onFile, ignorePosix)
48
+ }
49
+
50
+ /**
51
+ * Внутрішній рекурсор. ignorePosix вже нормалізовано — не нормалізуємо повторно на кожному рівні.
52
+ * @param {string} dir
53
+ * @param {(filePath: string) => void} onFile
54
+ * @param {string[]} ignorePosix
15
55
  * @returns {Promise<void>}
16
56
  */
17
- export async function walkDir(dir, onFile) {
57
+ async function walkDirInner(dir, onFile, ignorePosix) {
58
+ if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(dir), ignorePosix)) return
18
59
  let entries
19
60
  try {
20
61
  entries = await readdir(dir, { withFileTypes: true })
@@ -31,9 +72,9 @@ export async function walkDir(dir, onFile) {
31
72
  e.name === 'coverage' ||
32
73
  e.name === '.turbo' ||
33
74
  e.name === '.next'
34
- if (!skipDir) {
35
- await walkDir(p, onFile)
36
- }
75
+ if (skipDir) continue
76
+ if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(p), ignorePosix)) continue
77
+ await walkDirInner(p, onFile, ignorePosix)
37
78
  } else if (e.isFile()) {
38
79
  onFile(p)
39
80
  }