@nitra/cursor 1.39.1 → 1.40.1

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
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.40.1] - 2026-05-31
4
+
5
+ ### Changed
6
+
7
+ - js-lint: oxlint-canonical.json тепер source-of-truth — прибрано генерацію (rebuild-oxlint-canonical.mjs, oxlint-canonical-skeleton.json, oxlint-rules.tsv)
8
+
9
+ ## [1.40.0] - 2026-05-31
10
+
11
+ ### Added
12
+
13
+ - lint: розділення на `n-cursor lint` (quick, по змінених файлах) і `n-cursor lint-ci` (повний, по всьому репо) — data-driven за полем `meta.json.lint` (quick/ci); виконавець кроку — `js/lint.mjs` правила; jscpd+knip винесено в нове правило `js-lint-ci` (фаза ci)
14
+
3
15
  ## [1.39.1] - 2026-05-31
4
16
 
5
17
  ### Changed
package/bin/n-cursor.js CHANGED
@@ -105,7 +105,7 @@ import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
105
105
  import { runSkillsCli } from '../scripts/skills-cli.mjs'
106
106
  import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
107
107
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
108
- import { runLintCli } from '../scripts/lib/run-lint-cli.mjs'
108
+ import { runLint } from '../scripts/lint-cli.mjs'
109
109
  import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
110
110
 
111
111
  const PACKAGE_NAME = '@nitra/cursor'
@@ -1464,9 +1464,12 @@ try {
1464
1464
  break
1465
1465
  }
1466
1466
  case 'lint': {
1467
- // Оркестратор lint-ланцюжка з вимірюванням часу на кожен крок (fail-fast).
1468
- // Замінює раніше використовуваний агрегатор `bun run lint-ga && bun run lint-js && …` у root package.json.
1469
- process.exitCode = runLintCli()
1467
+ process.exitCode = await runLint({ ci: false })
1468
+
1469
+ break
1470
+ }
1471
+ case 'lint-ci': {
1472
+ process.exitCode = await runLint({ ci: true })
1470
1473
 
1471
1474
  break
1472
1475
  }
@@ -1540,7 +1543,7 @@ try {
1540
1543
  default: {
1541
1544
  console.error(`❌ Невідома команда: ${command}`)
1542
1545
  console.error(
1543
- ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree`
1546
+ ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci`
1544
1547
  )
1545
1548
  process.exitCode = 1
1546
1549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.39.1",
3
+ "version": "1.40.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ci-крок ga: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
3
+ */
4
+ import { runLintGaCli } from '../lint/lint.mjs'
5
+
6
+ /**
7
+ * @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
8
+ * @returns {Promise<number>} exit code
9
+ */
10
+ export async function lint(_files) {
11
+ return runLintGaCli()
12
+ }
@@ -1 +1 @@
1
- { "auto": { "glob": ".github/workflows/**" } }
1
+ { "auto": { "glob": ".github/workflows/**" }, "lint": "ci" }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Quick-крок lint правила js-lint: oxlint + eslint (з автофіксом).
3
+ *
4
+ * Викликається lint-оркестратором (`n-cursor lint` / `lint-ci`):
5
+ * - `files` = масив змінених файлів (quick) → лінтимо лише js-подібні з них;
6
+ * - `files` = undefined (ci) → лінтимо весь проєкт.
7
+ * Крос-файлові jscpd/knip — окреме правило js-lint-ci (фаза ci).
8
+ */
9
+ import { spawnSync } from 'node:child_process'
10
+
11
+ const JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u
12
+
13
+ /**
14
+ * Лишає лише js-подібні файли зі списку.
15
+ * @param {string[]} files список шляхів
16
+ * @returns {string[]} підмножина js-подібних
17
+ */
18
+ export function filterJsFiles(files) {
19
+ return files.filter(f => JS_EXT_RE.test(f))
20
+ }
21
+
22
+ /**
23
+ * @param {string[]} args аргументи інструмента (бінар через bunx)
24
+ * @param {string} cwd корінь
25
+ * @returns {number} exit code
26
+ */
27
+ function run(args, cwd) {
28
+ const r = spawnSync('bunx', args, { cwd, stdio: 'inherit' })
29
+ return typeof r.status === 'number' ? r.status : 1
30
+ }
31
+
32
+ /**
33
+ * Запускає oxlint+eslint з автофіксом.
34
+ * @param {string[] | undefined} files quick: лише ці файли; undefined: весь проєкт
35
+ * @param {string} [cwd] корінь репо
36
+ * @returns {Promise<number>} 0 — OK, ≠0 — порушення
37
+ */
38
+ export function lint(files, cwd = process.cwd()) {
39
+ let oxArgs = ['oxlint', '--fix']
40
+ let esArgs = ['eslint', '--fix']
41
+ if (files === undefined) {
42
+ esArgs.push('.')
43
+ } else {
44
+ const js = filterJsFiles(files)
45
+ if (js.length === 0) return Promise.resolve(0)
46
+ oxArgs = ['oxlint', '--fix', ...js]
47
+ esArgs = ['eslint', '--fix', ...js]
48
+ }
49
+ const ox = run(oxArgs, cwd)
50
+ if (ox !== 0) return Promise.resolve(ox)
51
+ return Promise.resolve(run(esArgs, cwd))
52
+ }
@@ -25,7 +25,7 @@ version: '1.26'
25
25
 
26
26
  У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
27
27
 
28
- У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/js/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./rules/js-lint/js/tooling/rebuild-oxlint-canonical.mjs`** (джерело правил **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
28
+ У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/js/data/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Канон **`oxlint-canonical.json`** — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
29
29
 
30
30
  Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
31
31
 
@@ -1 +1 @@
1
- { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } }
1
+ { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "quick" }
@@ -0,0 +1,19 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
2
+ import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
3
+
4
+ /**
5
+ * Запускає правило: applies → JS-concerns → policy → mdc-refs (через runStandardRule).
6
+ * Library mode: викликається CLI orchestration через `import + run(ctx)`.
7
+ * @param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext} [ctx] контекст прогону (walkCache тощо)
8
+ * @returns {Promise<number>} 0 — OK, 1 — порушення
9
+ */
10
+ export function run(ctx) {
11
+ return runStandardRule(import.meta.dirname, ctx)
12
+ }
13
+
14
+ if (isRunAsCli(import.meta.url)) {
15
+ // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
+ // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
+ process.exit(await runRuleCli(import.meta.dirname))
19
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Ci-крок: jscpd (детектор клонів) + knip (невикористані експорти).
3
+ *
4
+ * Крос-файлові аналізатори — працюють лише по всьому репо, тож `files` ігнорується
5
+ * (викликається лише у `lint-ci` з undefined). Per-file режиму ці інструменти не мають.
6
+ */
7
+ import { spawnSync } from 'node:child_process'
8
+
9
+ /**
10
+ * @param {string[] | undefined} _files ігнорується (крос-файловий аналіз)
11
+ * @param {string} [cwd] корінь репо
12
+ * @returns {Promise<number>} 0 — OK, ≠0 — порушення
13
+ */
14
+ export function lint(_files, cwd = process.cwd()) {
15
+ const jscpd = spawnSync('bunx', ['jscpd', '.'], { cwd, stdio: 'inherit' })
16
+ const jc = typeof jscpd.status === 'number' ? jscpd.status : 1
17
+ if (jc !== 0) return Promise.resolve(jc)
18
+ const knip = spawnSync('bunx', ['knip', '--no-config-hints'], { cwd, stdio: 'inherit' })
19
+ return Promise.resolve(typeof knip.status === 'number' ? knip.status : 1)
20
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Крос-файловий ci-етап js-lint — jscpd (детектор клонів) і knip (невикористані експорти). Лише у lint-ci, по всьому репо.
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # js-lint-ci — крос-файловий ci-етап
8
+
9
+ `jscpd` і `knip` аналізують увесь граф проєкту, тож мають сенс лише у повному прогоні
10
+ `npx @nitra/cursor lint-ci` (не у швидкому `lint` по змінених файлах). Per-file режиму нема.
11
+
12
+ Швидкий етап js-lint (oxlint/eslint) — у правилі `js-lint` (`lint: quick`).
@@ -0,0 +1 @@
1
+ { "lint": "ci" }
@@ -12,7 +12,7 @@ import { existsSync, readdirSync } from 'node:fs'
12
12
  import { join } from 'node:path'
13
13
 
14
14
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
15
- import { parseRuleAutoSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
15
+ import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
16
16
  import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs'
17
17
 
18
18
  /**
@@ -54,6 +54,15 @@ export function check(cwd = process.cwd()) {
54
54
  ruleOk = false
55
55
  }
56
56
  }
57
+ if (raw.lint !== undefined) {
58
+ if (parseRuleLintPhase(raw.lint) === null) {
59
+ reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
60
+ ruleOk = false
61
+ } else if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
62
+ reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
63
+ ruleOk = false
64
+ }
65
+ }
57
66
  if (ruleOk) {
58
67
  reporter.pass(`rules/${id}: meta.json валідний`)
59
68
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ci-крок rego: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
3
+ */
4
+ import { runLintRego } from '../lint/lint.mjs'
5
+
6
+ /**
7
+ * @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
8
+ * @returns {Promise<number>} exit code
9
+ */
10
+ export async function lint(_files) {
11
+ return runLintRego()
12
+ }
@@ -1 +1 @@
1
- { "auto": { "glob": "**/*.rego" } }
1
+ { "auto": { "glob": "**/*.rego" }, "lint": "ci" }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ci-крок security: trufflehog filesystem скан усього репо (per-file немає).
3
+ */
4
+ import { spawnSync } from 'node:child_process'
5
+
6
+ /**
7
+ * @param {string[] | undefined} _files ігнорується
8
+ * @param {string} [cwd] корінь
9
+ * @returns {Promise<number>} exit code
10
+ */
11
+ export function lint(_files, cwd = process.cwd()) {
12
+ const r = spawnSync(
13
+ 'trufflehog',
14
+ ['filesystem', '.', '--no-update', '--exclude-paths', '.trufflehog-exclude', '--results=verified,unknown', '--fail'],
15
+ { cwd, stdio: 'inherit' }
16
+ )
17
+ return Promise.resolve(typeof r.status === 'number' ? r.status : 1)
18
+ }
@@ -1 +1 @@
1
- { "auto": "завжди" }
1
+ { "auto": "завжди", "lint": "ci" }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Quick-крок lint правила style-lint: stylelint --fix по css/scss/vue.
3
+ *
4
+ * `files` (quick) → лише style-файли з них; undefined (ci) → весь glob `**\/*.{css,scss,vue}`.
5
+ */
6
+ import { spawnSync } from 'node:child_process'
7
+
8
+ const STYLE_EXT_RE = /\.(?:css|scss|vue)$/u
9
+
10
+ /**
11
+ * @param {string[]} files список шляхів
12
+ * @returns {string[]} лише css/scss/vue
13
+ */
14
+ export function filterStyleFiles(files) {
15
+ return files.filter(f => STYLE_EXT_RE.test(f))
16
+ }
17
+
18
+ /**
19
+ * @param {string[] | undefined} files quick: ці файли; undefined: весь проєкт
20
+ * @param {string} [cwd] корінь
21
+ * @returns {Promise<number>} exit code
22
+ */
23
+ export function lint(files, cwd = process.cwd()) {
24
+ const args = ['stylelint', '--fix']
25
+ if (files === undefined) {
26
+ args.push('**/*.{css,scss,vue}')
27
+ } else {
28
+ const style = filterStyleFiles(files)
29
+ if (style.length === 0) return Promise.resolve(0)
30
+ args.push(...style)
31
+ }
32
+ const r = spawnSync('npx', args, { cwd, stdio: 'inherit' })
33
+ return Promise.resolve(typeof r.status === 'number' ? r.status : 1)
34
+ }
@@ -1 +1 @@
1
- { "auto": { "glob": ["**/*.css", "**/*.vue"] } }
1
+ { "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "quick" }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ci-крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
3
+ */
4
+ import { runLintTextCli } from '../lint/lint.mjs'
5
+
6
+ /**
7
+ * @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
8
+ * @returns {Promise<number>} exit code
9
+ */
10
+ export async function lint(_files) {
11
+ return runLintTextCli()
12
+ }
@@ -1 +1 @@
1
- { "auto": "завжди" }
1
+ { "auto": "завжди", "lint": "ci" }
@@ -6,6 +6,7 @@
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
9
+ "lint": { "type": "string", "enum": ["quick", "ci"], "description": "Фаза lint-кроку: quick (по змінених, у lint і lint-ci) або ci (лише lint-ci)." },
9
10
  "auto": {
10
11
  "description": "Умова автоактивації правила: \"завжди\", масив id правил-залежностей, glob, або іменований предикат.",
11
12
  "oneOf": [
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Збір змінених файлів для quick-режиму lint-оркестратора.
3
+ *
4
+ * Quick лінтить лише те, що змінено в робочому дереві: tracked-modified + staged
5
+ * (`git diff HEAD`) і нові untracked (`git ls-files --others --exclude-standard`).
6
+ * Видалені файли не повертаються. Поза git-репо або при помилці git — порожній список.
7
+ */
8
+ import { spawnSync } from 'node:child_process'
9
+
10
+ /**
11
+ * @param {string[]} args аргументи git
12
+ * @param {string} cwd корінь
13
+ * @returns {string[]} непорожні рядки stdout або [] при помилці
14
+ */
15
+ function gitLines(args, cwd) {
16
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8' })
17
+ if (r.status !== 0 || r.error) return []
18
+ return r.stdout.split('\n').map(s => s.trim()).filter(Boolean)
19
+ }
20
+
21
+ /**
22
+ * Relative-posix список змінених + untracked файлів робочого дерева.
23
+ * @param {string} [cwd] корінь репо
24
+ * @returns {string[]} унікальні шляхи (без видалених)
25
+ */
26
+ export function collectChangedFiles(cwd = process.cwd()) {
27
+ const modified = gitLines(['diff', 'HEAD', '--name-only', '--diff-filter=ACMR'], cwd)
28
+ const untracked = gitLines(['ls-files', '--others', '--exclude-standard'], cwd)
29
+ return [...new Set([...modified, ...untracked])]
30
+ }
@@ -48,6 +48,18 @@ export function parseRuleAutoSpec(value) {
48
48
  return null
49
49
  }
50
50
 
51
+ /** Допустимі фази lint. */
52
+ const LINT_PHASES = new Set(['quick', 'ci'])
53
+
54
+ /**
55
+ * Нормалізує значення `meta.json.lint` у фазу lint.
56
+ * @param {unknown} value значення поля `lint`
57
+ * @returns {'quick' | 'ci' | null} фаза або `null` (відсутнє/невалідне = не lint-крок)
58
+ */
59
+ export function parseRuleLintPhase(value) {
60
+ return typeof value === 'string' && LINT_PHASES.has(value) ? /** @type {'quick'|'ci'} */ (value) : null
61
+ }
62
+
51
63
  /**
52
64
  * Читає й парсить `meta.json` одного правила.
53
65
  * @param {string} ruleDir абсолютний шлях до каталогу правила
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Оркестратор `n-cursor lint` (quick) / `n-cursor lint-ci` (full).
3
+ *
4
+ * Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`quick`|`ci`),
5
+ * послідовно (заборона паралельного eslint) викликає `rules/<id>/js/lint.mjs`:
6
+ * - quick: `lint(changedFiles)` — лише змінені файли (git diff HEAD + untracked);
7
+ * - ci: `lint(undefined)` — весь проєкт.
8
+ * Порядок правил — алфавітний; ci-набір = quick ∪ ci. Fail-fast: перший ненульовий код спиняє.
9
+ */
10
+ import { existsSync, readdirSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { cwd as processCwd } from 'node:process'
14
+
15
+ import { parseRuleLintPhase, readRuleMetaRaw } from './lib/rule-meta.mjs'
16
+ import { collectChangedFiles } from './lib/changed-files.mjs'
17
+
18
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
19
+ const RULES_DIR = join(PACKAGE_ROOT, 'rules')
20
+
21
+ /**
22
+ * Вибирає id правил для фази, алфавітно.
23
+ * @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт
24
+ * @param {'quick'|'ci'} phase цільова фаза (quick → лише quick; ci → quick+ci)
25
+ * @returns {string[]} відсортовані id
26
+ */
27
+ export function selectLintRules(metaById, phase) {
28
+ const out = []
29
+ for (const [id, raw] of Object.entries(metaById)) {
30
+ const p = parseRuleLintPhase(raw?.lint)
31
+ if (p === 'quick' || (phase === 'ci' && p === 'ci')) out.push(id)
32
+ }
33
+ return out.toSorted((a, b) => a.localeCompare(b))
34
+ }
35
+
36
+ /**
37
+ * Зчитує meta всіх правил пакета.
38
+ * @param {string} rulesDir каталог rules
39
+ * @returns {Record<string, Record<string, unknown>>} id → meta
40
+ */
41
+ function readAllMeta(rulesDir) {
42
+ /** @type {Record<string, Record<string, unknown>>} */
43
+ const out = {}
44
+ if (!existsSync(rulesDir)) return out
45
+ for (const e of readdirSync(rulesDir, { withFileTypes: true })) {
46
+ if (!e.isDirectory() || e.name.startsWith('.')) continue
47
+ const raw = readRuleMetaRaw(join(rulesDir, e.name))
48
+ if (raw) out[e.name] = raw
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * Запускає lint-оркестрацію.
55
+ * @param {{ ci?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
56
+ * @returns {Promise<number>} exit code
57
+ */
58
+ export async function runLint(opts = {}) {
59
+ const ci = opts.ci === true
60
+ const cwd = opts.cwd ?? processCwd()
61
+ const rulesDir = opts.rulesDir ?? RULES_DIR
62
+ const log = opts.log ?? (s => process.stdout.write(s))
63
+
64
+ const changed = ci ? undefined : collectChangedFiles(cwd)
65
+ if (!ci && changed.length === 0) {
66
+ log('\nℹ️ lint: немає змінених файлів — нічого перевіряти.\n')
67
+ return 0
68
+ }
69
+
70
+ const ids = selectLintRules(readAllMeta(rulesDir), ci ? 'ci' : 'quick')
71
+ for (const id of ids) {
72
+ const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
73
+ if (!existsSync(lintPath)) {
74
+ log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
75
+ continue
76
+ }
77
+ // eslint-disable-next-line no-unsanitized/method -- шлях з discovered rule dir
78
+ const mod = await import(lintPath)
79
+ const code = await mod.lint(changed, cwd)
80
+ if (code !== 0) return code
81
+ }
82
+ return 0
83
+ }
@@ -1,27 +0,0 @@
1
- {
2
- "$schema": "./node_modules/oxlint/configuration_schema.json",
3
- "plugins": ["unicorn", "oxc", "import", "jsdoc", "promise", "node", "vue"],
4
- "jsPlugins": ["@e18e/eslint-plugin"],
5
- "categories": {},
6
- "rules": {},
7
- "settings": {
8
- "next": {
9
- "rootDir": []
10
- },
11
- "jsdoc": {
12
- "ignorePrivate": false,
13
- "ignoreInternal": false,
14
- "ignoreReplacesDocs": true,
15
- "overrideReplacesDocs": true,
16
- "augmentsExtendsReplacesDocs": false,
17
- "implementsReplacesDocs": false,
18
- "exemptDestructuredRootsFromChecks": false,
19
- "tagNamePreference": {}
20
- }
21
- },
22
- "env": {
23
- "builtin": true
24
- },
25
- "globals": {},
26
- "ignorePatterns": ["**/schema.graphql", "**/auto-imports.d.ts"]
27
- }
@@ -1,359 +0,0 @@
1
- e18e/prefer-includes error
2
- array-callback-return deny
3
- no-new-wrappers deny
4
- import/no-named-as-default deny
5
- oxc/uninvoked-array-callback deny
6
- typescript/no-non-null-assertion deny
7
- unicorn/prefer-date-now deny
8
- no-obj-calls deny
9
- default-case-last deny
10
- import/no-webpack-loader-syntax deny
11
- promise/no-promise-in-callback deny
12
- typescript/no-unnecessary-type-constraint deny
13
- unicorn/prefer-dom-node-remove deny
14
- for-direction deny
15
- no-prototype-builtins deny
16
- typescript/no-useless-empty-export deny
17
- unicorn/prefer-includes deny
18
- guard-for-in deny
19
- no-restricted-globals deny
20
- promise/prefer-await-to-then off
21
- typescript/prefer-as-const deny
22
- unicorn/prefer-math-trunc deny
23
- no-self-assign deny
24
- max-params off
25
- react/button-has-type deny
26
- typescript/prefer-function-type deny
27
- unicorn/prefer-native-coercion-functions deny
28
- no-extra-label deny
29
- no-shadow-restricted-names deny
30
- react/iframe-missing-sandbox deny
31
- typescript/prefer-ts-expect-error deny
32
- unicorn/prefer-number-properties off
33
- no-labels deny
34
- no-ternary off
35
- react/jsx-key deny
36
- unicorn/consistent-empty-array-spread deny
37
- unicorn/prefer-query-selector deny
38
- no-object-constructor deny
39
- react/jsx-no-script-url deny
40
- unicorn/empty-brace-spaces deny
41
- unicorn/prefer-set-has deny
42
- no-array-constructor deny
43
- react/jsx-no-useless-fragment deny
44
- unicorn/explicit-length-check off
45
- unicorn/prefer-string-replace-all deny
46
- no-bitwise deny
47
- no-unsafe-optional-chaining deny
48
- react/no-children-prop deny
49
- unicorn/no-abusive-eslint-disable deny
50
- unicorn/prefer-string-trim-start-end deny
51
- no-class-assign deny
52
- no-unused-private-class-members deny
53
- react/no-direct-mutation-state deny
54
- unicorn/no-array-reduce deny
55
- unicorn/require-array-join-separator deny
56
- no-console off
57
- no-useless-concat deny
58
- react/no-render-return-value deny
59
- unicorn/no-console-spaces deny
60
- unicorn/text-encoding-identifier-case deny
61
- no-constant-condition deny
62
- no-useless-rename deny
63
- react/no-unescaped-entities deny
64
- unicorn/no-hex-escape deny
65
- no-control-regex deny
66
- no-with deny
67
- react/react-in-jsx-scope deny
68
- unicorn/no-length-as-slice-end deny
69
- no-div-regex deny
70
- prefer-numeric-literals deny
71
- react/self-closing-comp deny
72
- unicorn/no-negation-in-equality-check deny
73
- no-dupe-keys deny
74
- prefer-rest-params deny
75
- unicorn/no-new-buffer deny
76
- no-empty-character-class deny
77
- require-await deny
78
- node/no-exports-assign deny
79
- unicorn/no-process-exit deny
80
- no-empty-static-block deny
81
- sort-keys off
82
- oxc/bad-array-method-on-arguments deny
83
- unicorn/no-thenable deny
84
- typescript/ban-ts-comment deny
85
- no-eval deny
86
- unicode-bom deny
87
- jsdoc/check-access deny
88
- oxc/bad-comparison-sequence deny
89
- unicorn/no-unnecessary-await deny
90
- typescript/consistent-generic-constructors deny
91
- no-extra-boolean-cast deny
92
- vars-on-top deny
93
- jsdoc/empty-tags deny
94
- oxc/bad-replace-all-arg deny
95
- unicorn/no-useless-fallback-in-spread deny
96
- no-global-assign deny
97
- jsdoc/require-param deny
98
- oxc/erasing-op deny
99
- unicorn/no-useless-spread deny
100
- typescript/no-confusing-non-null-assertion deny
101
- no-invalid-regexp deny
102
- import/no-namespace off
103
- jsdoc/require-param-type deny
104
- oxc/no-accumulating-spread deny
105
- typescript/no-empty-interface deny
106
- unicorn/no-zero-fractions deny
107
- import/namespace deny
108
- jsdoc/require-property-name deny
109
- no-label-var deny
110
- typescript/no-extra-non-null-assertion deny
111
- oxc/no-barrel-file deny
112
- unicorn/prefer-spread deny
113
- no-negated-condition deny
114
- import/no-cycle deny
115
- jsdoc/require-returns-description deny
116
- typescript/no-misused-new deny
117
- oxc/no-optional-chaining off
118
- unicorn/prefer-array-flat-map deny
119
- no-new-native-nonconstructor deny
120
- import/no-dynamic-require deny
121
- oxc/only-used-in-recursion deny
122
- typescript/no-non-null-asserted-optional-chain deny
123
- unicorn/prefer-code-point deny
124
- default-case off
125
- no-nonoctal-decimal-escape deny
126
- import/no-self-import deny
127
- promise/catch-or-return deny
128
- typescript/no-this-alias deny
129
- unicorn/prefer-dom-node-dataset deny
130
- eqeqeq deny
131
- no-proto deny
132
- promise/no-new-statics deny
133
- typescript/no-unsafe-function-type deny
134
- unicorn/prefer-event-target deny
135
- no-regex-spaces deny
136
- promise/prefer-await-to-callbacks off
137
- typescript/no-wrapper-object-types deny
138
- unicorn/prefer-math-min-max deny
139
- max-lines off
140
- no-script-url deny
141
- promise/valid-params deny
142
- typescript/prefer-for-of deny
143
- unicorn/prefer-modern-math-apis deny
144
- no-useless-call deny
145
- no-setter-return deny
146
- typescript/prefer-namespace-keyword deny
147
- unicorn/prefer-node-protocol deny
148
- no-nested-ternary off
149
- no-template-curly-in-string deny
150
- react/jsx-curly-brace-presence deny
151
- unicorn/catch-error-name deny
152
- unicorn/prefer-prototype-methods deny
153
- no-throw-literal deny
154
- react/jsx-no-duplicate-props deny
155
- unicorn/consistent-function-scoping deny
156
- unicorn/prefer-regexp-test deny
157
- no-alert deny
158
- no-unexpected-multiline deny
159
- react/jsx-no-undef deny
160
- unicorn/escape-case deny
161
- unicorn/prefer-string-raw deny
162
- no-await-in-loop off
163
- no-unsafe-negation deny
164
- react/no-array-index-key deny
165
- unicorn/new-for-builtins deny
166
- unicorn/prefer-string-starts-ends-with deny
167
- no-case-declarations deny
168
- no-unused-labels deny
169
- react/no-danger deny
170
- unicorn/no-array-for-each deny
171
- unicorn/prefer-type-error deny
172
- no-cond-assign deny
173
- no-useless-catch deny
174
- react/no-is-mounted deny
175
- unicorn/no-await-in-promise-methods deny
176
- unicorn/switch-case-braces deny
177
- no-constant-binary-expression deny
178
- no-useless-escape deny
179
- react/no-string-refs deny
180
- unicorn/no-empty-file deny
181
- no-void deny
182
- unicorn/no-invalid-remove-event-listener deny
183
- react/prefer-es6-class deny
184
- no-delete-var deny
185
- prefer-exponentiation-operator deny
186
- react/rules-of-hooks deny
187
- unicorn/no-magic-array-flat-depth deny
188
- no-dupe-else-if deny
189
- prefer-object-spread deny
190
- react/void-dom-elements-no-children deny
191
- unicorn/no-new-array deny
192
- no-else-return deny
193
- radix off
194
- unicorn/no-object-as-default-parameter deny
195
- no-empty-pattern deny
196
- sort-imports off
197
- oxc/approx-constant deny
198
- typescript/array-type deny
199
- unicorn/no-static-only-class deny
200
- no-eq-null deny
201
- symbol-description deny
202
- oxc/bad-char-at-comparison deny
203
- typescript/ban-types deny
204
- unicorn/no-typeof-undefined deny
205
- no-extend-native deny
206
- valid-typeof deny
207
- jsdoc/check-tag-names deny
208
- oxc/bad-object-literal-comparison deny
209
- typescript/consistent-type-definitions deny
210
- unicorn/no-unreadable-iife deny
211
- no-func-assign deny
212
- import/default deny
213
- jsdoc/no-defaults deny
214
- oxc/double-comparisons deny
215
- typescript/no-inferrable-types deny
216
- unicorn/no-useless-promise-resolve-reject deny
217
- no-inner-declarations deny
218
- import/no-named-default deny
219
- oxc/missing-throw deny
220
- jsdoc/require-param-name deny
221
- typescript/no-dynamic-delete deny
222
- unicorn/no-useless-undefined deny
223
- no-iterator deny
224
- jsdoc/require-property-description deny
225
- oxc/no-async-endpoint-handlers deny
226
- typescript/no-explicit-any deny
227
- unicorn/numeric-separators-style off
228
- no-magic-numbers off
229
- import/no-commonjs off
230
- jsdoc/require-returns deny
231
- typescript/no-import-type-side-effects deny
232
- unicorn/prefer-array-flat deny
233
- no-new-func deny
234
- import/no-duplicates deny
235
- jsdoc/require-yields deny
236
- oxc/number-arg-out-of-range deny
237
- typescript/no-non-null-asserted-nullish-coalescing deny
238
- unicorn/prefer-blob-reading-methods deny
239
- no-new deny
240
- import/no-named-as-default-member off
241
- promise/avoid-new deny
242
- typescript/no-require-imports deny
243
- unicorn/prefer-dom-node-append deny
244
- default-param-last deny
245
- no-plusplus off
246
- import/unambiguous off
247
- promise/no-callback-in-promise deny
248
- typescript/no-unsafe-declaration-merging deny
249
- unicorn/prefer-dom-node-text-content deny
250
- func-names off
251
- no-redeclare deny
252
- promise/param-names deny
253
- typescript/no-var-requires deny
254
- unicorn/prefer-logical-operator-over-ternary deny
255
- max-classes-per-file deny
256
- no-return-assign deny
257
- promise/spec-only deny
258
- typescript/prefer-enum-initializers deny
259
- unicorn/prefer-modern-dom-apis deny
260
- new-cap off
261
- no-self-compare deny
262
- react/checked-requires-onchange-or-readonly deny
263
- typescript/prefer-literal-enum-member deny
264
- unicorn/prefer-negative-index deny
265
- no-multi-assign deny
266
- no-sparse-arrays deny
267
- typescript/triple-slash-reference deny
268
- react/jsx-boolean-value deny
269
- unicorn/prefer-optional-catch-binding deny
270
- no-lone-blocks deny
271
- no-this-before-super deny
272
- react/jsx-no-comment-textnodes deny
273
- unicorn/consistent-existence-index-check deny
274
- unicorn/prefer-reflect-apply deny
275
- no-duplicate-imports deny
276
- no-undefined off
277
- react/jsx-no-target-blank deny
278
- unicorn/prefer-set-size deny
279
- unicorn/error-message deny
280
- no-async-promise-executor deny
281
- no-unsafe-finally deny
282
- react/jsx-props-no-spread-multi deny
283
- unicorn/filename-case off
284
- unicorn/prefer-string-slice deny
285
- no-caller deny
286
- no-unused-expressions deny
287
- react/no-danger-with-children deny
288
- unicorn/no-anonymous-default-export off
289
- unicorn/prefer-structured-clone deny
290
- no-compare-neg-zero deny
291
- no-unused-vars deny
292
- react/no-find-dom-node deny
293
- unicorn/no-await-expression-member deny
294
- unicorn/require-number-to-fixed-digits-argument deny
295
- no-const-assign deny
296
- no-useless-constructor deny
297
- react/no-set-state deny
298
- unicorn/no-document-cookie deny
299
- unicorn/throw-new-error deny
300
- no-constructor-return deny
301
- no-var deny
302
- react/no-unknown-property deny
303
- unicorn/no-instanceof-array deny
304
- no-debugger deny
305
- prefer-promise-reject-errors deny
306
- unicorn/no-lonely-if deny
307
- no-dupe-class-members deny
308
- prefer-object-has-own deny
309
- react/style-prop-object deny
310
- unicorn/no-nested-ternary off
311
- no-duplicate-case deny
312
- prefer-spread deny
313
- unicorn/no-null off
314
- no-empty-function deny
315
- require-yield deny
316
- node/no-new-require deny
317
- typescript/adjacent-overload-signatures deny
318
- unicorn/no-single-promise-in-promise-methods deny
319
- no-empty deny
320
- sort-vars deny
321
- oxc/bad-bitwise-operator deny
322
- typescript/ban-tslint-comment deny
323
- unicorn/no-this-assignment deny
324
- no-ex-assign deny
325
- use-isnan deny
326
- jsdoc/check-property-names deny
327
- oxc/bad-min-max-func deny
328
- typescript/consistent-indexed-object-style deny
329
- unicorn/no-unreadable-array-destructuring deny
330
- no-fallthrough deny
331
- yoda deny
332
- jsdoc/implements-on-classes deny
333
- oxc/const-comparisons deny
334
- unicorn/no-useless-length-check deny
335
- typescript/explicit-function-return-type deny
336
- no-import-assign deny
337
- import/first deny
338
- jsdoc/require-param-description deny
339
- typescript/no-duplicate-enum-values deny
340
- oxc/misrefactored-assign-op deny
341
- unicorn/no-useless-switch-case deny
342
- no-irregular-whitespace deny
343
- import/max-dependencies off
344
- jsdoc/require-property deny
345
- oxc/no-async-await off
346
- typescript/no-empty-object-type deny
347
- unicorn/number-literal-case deny
348
- no-loss-of-precision deny
349
- import/no-amd deny
350
- jsdoc/require-property-type deny
351
- oxc/no-const-enum deny
352
- typescript/no-extraneous-class deny
353
- unicorn/prefer-add-event-listener deny
354
- no-multi-str deny
355
- import/no-default-export off
356
- jsdoc/require-returns-type deny
357
- oxc/no-rest-spread-properties off
358
- typescript/no-namespace deny
359
- unicorn/prefer-array-some deny
@@ -1,29 +0,0 @@
1
- /**
2
- * Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
3
- * правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
4
- *
5
- * Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/lib/rebuild-oxlint-canonical.mjs`,
6
- * потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
7
- */
8
- import { readFileSync, writeFileSync } from 'node:fs'
9
- import { dirname, join } from 'node:path'
10
- import { fileURLToPath } from 'node:url'
11
-
12
- const dir = join(dirname(fileURLToPath(import.meta.url)), '..', 'js', 'data', 'tooling')
13
- const rules = {}
14
- for (const line of readFileSync(join(dir, 'oxlint-rules.tsv'), 'utf8').split('\n')) {
15
- const t = line.trim()
16
- if (!t) {
17
- continue
18
- }
19
- const i = t.indexOf('\t')
20
- if (i === -1) {
21
- throw new Error(`oxlint-rules.tsv: очікується TAB між ключем і значенням: ${t}`)
22
- }
23
- rules[t.slice(0, i)] = t.slice(i + 1)
24
- }
25
- const skeleton = JSON.parse(readFileSync(join(dir, 'oxlint-canonical-skeleton.json'), 'utf8'))
26
- skeleton.rules = rules
27
- const out = join(dir, 'oxlint-canonical.json')
28
- writeFileSync(out, `${JSON.stringify(skeleton, null, 2)}\n`)
29
- console.log(`wrote ${out} (${Object.keys(rules).length} rules)`)
@@ -1,116 +0,0 @@
1
- /**
2
- * `n-cursor lint` — оркестратор лінт-ланцюжка з вимірюванням часу на кожен крок.
3
- *
4
- * Замість агрегатора `bun run lint-ga && bun run lint-js && ... && oxfmt .` у кореневому
5
- * `package.json` (де child-процеси анонімні і час кожного не видно), цей оркестратор:
6
- *
7
- * - читає `scripts` з кореневого `package.json`,
8
- * - бере **присутні** ключі з фіксованого списку `LINT_SCRIPTS` (відсутні мовчки пропускає),
9
- * - послідовно запускає `bun run <script>`,
10
- * - заміряє час кожного,
11
- * - **fail-fast**: при першому ненульовому exit-коді зупиняється, друкує таблицю
12
- * лише по виконаних і повертає той самий код,
13
- * - друкує підсумкову таблицю `⏱ Lint timing` і повертає 0, якщо все ОК.
14
- *
15
- * Список + порядок зумисне фіксований: збігається з канонічним ланцюжком, що його раніше
16
- * тримав root `package.json`. Динамічний discovery (`scripts/^lint-/`) дав би непередбачуваний
17
- * порядок і небажану інтерпретацію власних `lint-*` користувача.
18
- *
19
- * `oxfmt` — окремий рядок поза префіксом `lint-`, ставиться в кінець (як було у `lint`).
20
- */
21
- import { spawnSync as defaultSpawnSync } from 'node:child_process'
22
- import { existsSync, readFileSync } from 'node:fs'
23
- import { join } from 'node:path'
24
-
25
- import { formatTimingSummary } from './timing-summary.mjs'
26
-
27
- /**
28
- * Імена npm-скриптів, які `n-cursor lint` запускає **по черзі**, якщо вони є у root `package.json`.
29
- * Порядок дзеркалить попередній агрегатор `lint`: cheap-checks першими, формат — в кінці.
30
- */
31
- export const LINT_SCRIPTS = /** @type {const} */ ([
32
- 'lint-ga',
33
- 'lint-js',
34
- 'lint-rego',
35
- 'lint-style',
36
- 'lint-text',
37
- 'lint-security',
38
- 'oxfmt'
39
- ])
40
-
41
- /**
42
- * Читає `scripts` з `package.json` у заданій теці. Повертає `null`, якщо файла немає, JSON
43
- * некоректний або поля `scripts` нема. Не кидає — викликач сам вирішує, що робити.
44
- * @param {string} root абсолютний шлях до теки з `package.json`
45
- * @returns {Record<string, string> | null} мапа scripts або null
46
- */
47
- function readRootScripts(root) {
48
- const packageJsonPath = join(root, 'package.json')
49
- if (!existsSync(packageJsonPath)) {
50
- return null
51
- }
52
- try {
53
- const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
54
- const scripts = parsed?.scripts
55
- if (!scripts || typeof scripts !== 'object') {
56
- return null
57
- }
58
- return /** @type {Record<string, string>} */ (scripts)
59
- } catch {
60
- return null
61
- }
62
- }
63
-
64
- /**
65
- * @typedef {{
66
- * cwd?: string,
67
- * spawnSyncFn?: typeof defaultSpawnSync,
68
- * now?: () => number,
69
- * log?: (text: string) => void,
70
- * logError?: (text: string) => void
71
- * }} RunLintCliOptions
72
- */
73
-
74
- /**
75
- * Виконує лінт-ланцюжок з вимірюванням часу. Повертає exit-код, не кидає винятків (для прямого
76
- * присвоєння у `process.exitCode`).
77
- * @param {RunLintCliOptions} [options] DI для тестів (підміняємо spawn / fs / clock)
78
- * @returns {number} 0 = успіх, ненульовий = code першого впалого скрипта, або 1 при структурних проблемах
79
- */
80
- export function runLintCli(options = {}) {
81
- const root = options.cwd ?? process.cwd()
82
- const spawnSync = options.spawnSyncFn ?? defaultSpawnSync
83
- const now = options.now ?? Date.now
84
- const log = options.log ?? (text => process.stdout.write(text))
85
- const logError = options.logError ?? (text => process.stderr.write(text))
86
-
87
- const scripts = readRootScripts(root)
88
- if (scripts === null) {
89
- logError(`❌ n-cursor lint: не знайдено package.json або поля "scripts" у ${root}\n`)
90
- return 1
91
- }
92
-
93
- const present = LINT_SCRIPTS.filter(name => typeof scripts[name] === 'string' && scripts[name].length > 0)
94
- if (present.length === 0) {
95
- log('\nℹ️ n-cursor lint: у package.json немає жодного з lint-* / oxfmt скриптів — нічого запускати.\n')
96
- return 0
97
- }
98
-
99
- /** @type {{ id: string, ms: number, ok: boolean }[]} */
100
- const timings = []
101
- let failedCode = 0
102
- for (const name of present) {
103
- const startedAt = now()
104
- const result = spawnSync('bun', ['run', name], { stdio: 'inherit', cwd: root })
105
- const code = typeof result.status === 'number' ? result.status : 1
106
- const ok = code === 0
107
- timings.push({ id: name, ms: now() - startedAt, ok })
108
- if (!ok) {
109
- failedCode = code === 0 ? 1 : code
110
- break
111
- }
112
- }
113
-
114
- log(formatTimingSummary('Lint timing', timings))
115
- return failedCode
116
- }