Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@nitra/cursor

Package Overview
Dependencies
Maintainers
1
Versions
397
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nitra/cursor - npm Package Compare versions

Comparing version
5.4.0
to
6.0.0
+1
-1
package.json
{
"name": "@nitra/cursor",
"version": "5.4.0",
"version": "6.0.0",
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",

@@ -5,0 +5,0 @@ "keywords": [

@@ -94,2 +94,15 @@ /**

/**
* Рядок таймінгу одного файлу: загальний час, час у LLM (і кількість викликів)
* та залишок — оркестрація (екстракт фактів, скоринг, парсинг, IO). Дає зрозуміти,
* скільки коштує сама модель проти JS-оркестрації.
* @param {{ ms: number, llmMs?: number, llmCalls?: number }} r результат generateDoc
* @returns {string} напр. `12.3s (llm 11.8s/7 calls, orch 0.5s)`
*/
function fmtTiming(r) {
const s = ms => `${(ms / 1000).toFixed(1)}s`
const llmMs = r.llmMs ?? 0
return `${s(r.ms)} (llm ${s(llmMs)}/${r.llmCalls ?? 0} calls, orch ${s(r.ms - llmMs)})`
}
/**
* Генерує й штампує доку для одного файлу, оновлюючи лічильники й прогрес.

@@ -118,5 +131,5 @@ * @param {object} file елемент scanForDocFiles

stats.degraded++
process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`)
process.stdout.write(`⚠ degraded score=${result.score} crc=${crc} ${fmtTiming(result)}\n`)
} else {
process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`)
process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc} ${fmtTiming(result)}\n`)
}

@@ -142,3 +155,3 @@ } catch (error) {

if (stats.degraded > 0) {
console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`)
console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor fix-doc-files --retry-degraded`)
}

@@ -170,3 +183,3 @@ }

if (problem) {
console.error(`✗ doc-files gen: ${problem}`)
console.error(`✗ fix-doc-files: ${problem}`)
return 1

@@ -208,3 +221,3 @@ }

}
console.log(`✓ doc-files stamp: оновлено frontmatter у ${stamped} доці(ах).`)
console.log(`✓ fix-doc-files --stamp: оновлено frontmatter у ${stamped} доці(ах).`)
return 0

@@ -211,0 +224,0 @@ }

@@ -7,3 +7,3 @@ /** @see ./docs/docgen-gen.md */

import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
import { callLlm } from '../../../lib/llm.mjs'
import { callLlm as callLlmRaw } from '../../../lib/llm.mjs'
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'

@@ -23,2 +23,22 @@ import { docPathForSource } from './docgen-scan.mjs'

/** Облік LLM-викликів і часу в них у межах однієї генерації (скидається на старті generateDoc). */
let llmMeter = { calls: 0, ms: 0 }
/**
* Обгортка callLlm з обліком: лічить кількість викликів і сумарний час у них.
* callLlm синхронний (spawnSync/curl), генерація одного файлу послідовна — лічильник без гонок.
* Усі виклики `callLlm(...)` у цьому модулі йдуть через неї автоматично (імпорт як callLlmRaw).
* @param {...any} args ті самі аргументи, що й у callLlm з lib/llm.mjs
* @returns {string} відповідь моделі
*/
function callLlm(...args) {
const started = Date.now()
try {
return callLlmRaw(...args)
} finally {
llmMeter.calls += 1
llmMeter.ms += Date.now() - started
}
}
const FENCE_OPEN_RE = /^```[a-z]*\n?/

@@ -365,3 +385,3 @@ const FENCE_CLOSE_RE = /\n?```\s*$/

* @param {{ model?: string, threshold?: number, existingMd?: string|null }} [opts] model-id, поріг degraded, наявна дока (для збереження захищеної секції)
* @returns {{ md: string, ms: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації
* @returns {{ md: string, ms: number, llmMs: number, llmCalls: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації (ms — увесь файл; llmMs/llmCalls — лише LLM; решта ms — оркестрація)
*/

@@ -372,2 +392,3 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUALITY_THRESHOLD, existingMd = null } = {}) {

const t0 = Date.now()
llmMeter = { calls: 0, ms: 0 }

@@ -383,3 +404,12 @@ // Варіант B: захищена секція «Призначення» з наявної доки — зберегти й подати як контекст

if (facts.unsupported) {
return { ...r, ms: Date.now() - t0, score: null, issues: [], degraded: false, model }
return {
...r,
ms: Date.now() - t0,
llmMs: llmMeter.ms,
llmCalls: llmMeter.calls,
score: null,
issues: [],
degraded: false,
model
}
}

@@ -407,3 +437,12 @@

return { ...r, ms: Date.now() - t0, score, issues, degraded: score < threshold, model }
return {
...r,
ms: Date.now() - t0,
llmMs: llmMeter.ms,
llmCalls: llmMeter.calls,
score,
issues,
degraded: score < threshold,
model
}
}

@@ -425,4 +464,6 @@

const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : ''
process.stderr.write(`[local ${r.model}] ${r.ms}ms / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`)
process.stderr.write(
`[local ${r.model}] ${r.ms}ms (llm ${r.llmMs}ms/${r.llmCalls} calls, orch ${r.ms - r.llmMs}ms) / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`
)
process.stdout.write(r.md)
}

@@ -232,3 +232,3 @@ /** @see ./docs/docgen-scan.md */

console.log(
`⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ перегенеруй: npx @nitra/cursor doc-files gen --retry-degraded`
`⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ перегенеруй: npx @nitra/cursor fix-doc-files --retry-degraded`
)

@@ -289,3 +289,3 @@ return 0

console.error(
`⚠ doc-files: застарілих док ${stale.length} (> ${gateMax}) — гейт не блокує. Запусти масовий прогін:\n npx @nitra/cursor doc-files gen`
`⚠ doc-files: застарілих док ${stale.length} (> ${gateMax}) — гейт не блокує. Запусти масовий прогін:\n npx @nitra/cursor fix-doc-files`
)

@@ -292,0 +292,0 @@ return 0

---
docgen:
source: npm/rules/doc-files/js/docgen-files-batch.mjs
crc: 5c9b8d72
crc: 6f01f8b9
score: 95

@@ -6,0 +6,0 @@ ---

---
docgen:
source: npm/rules/doc-files/js/docgen-gen.mjs
crc: e2af04d6
crc: 70215974
score: 100

@@ -6,0 +6,0 @@ ---

---
docgen:
source: npm/rules/doc-files/js/docgen-scan.mjs
crc: 46f11827
crc: dcc90d44
score: 100

@@ -6,0 +6,0 @@ ---

@@ -1,1 +0,1 @@

{ "auto": "завжди", "lint": "quick" }
{ "auto": "завжди", "lint": "per-file" }

@@ -1,1 +0,1 @@

{ "auto": { "glob": ".github/workflows/**" }, "lint": "ci" }
{ "auto": { "glob": ".github/workflows/**" }, "lint": "full" }

@@ -1,1 +0,1 @@

{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "ci" }
{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "full" }
---
docgen:
source: npm/rules/js-lint/js/lint.mjs
crc: c90c15eb
crc: 1f38613e
score: 100

@@ -6,0 +6,0 @@ ---

@@ -57,10 +57,11 @@ /** @see ./docs/lint.md */

/**
* Full-режим (ci): лінт усього проєкту зі стрімінгом і fail-fast (без класифікації).
* Full-режим (--full): лінт усього проєкту зі стрімінгом і fail-fast (без класифікації).
* @param {string} cwd корінь
* @param {boolean} readOnly true → без `--fix` (детект, нуль мутацій — CI)
* @returns {number} exit code
*/
function lintFullProject(cwd) {
const ox = runInherit(['oxlint', '--fix'], cwd)
function lintFullProject(cwd, readOnly) {
const ox = runInherit(readOnly ? ['oxlint'] : ['oxlint', '--fix'], cwd)
if (ox !== 0) return ox
return runInherit(['eslint', '--fix', '.'], cwd)
return runInherit(readOnly ? ['eslint', '.'] : ['eslint', '--fix', '.'], cwd)
}

@@ -73,8 +74,12 @@

* @param {string} cwd корінь
* @param {boolean} readOnly true → пропустити фікс-пас (детект, нуль мутацій)
* @returns {number} exit code (0 — чисто; 1 — лишились findings)
*/
function lintChangedClassified(js, cwd) {
function lintChangedClassified(js, cwd, readOnly) {
// Фікс-пас обох інструментів (послідовно; обидва — щоб репорт показав повну картину).
runFix(['oxlint', '--fix', ...js], cwd)
runFix(['eslint', '--fix', ...js], cwd)
// У read-only пропускаємо — лише детект без мутацій (CI / pre-commit).
if (!readOnly) {
runFix(['oxlint', '--fix', ...js], cwd)
runFix(['eslint', '--fix', ...js], cwd)
}

@@ -104,14 +109,16 @@ // Репорт-пас по ФІНАЛЬНОМУ (пост-фікс) файлу — рядки findings і diff узгоджені.

/**
* Запускає oxlint+eslint з автофіксом.
* @param {string[] | undefined} files quick: лише ці файли; undefined: весь проєкт
* Запускає oxlint+eslint. За замовчуванням — з автофіксом; `opts.readOnly` — лише детект.
* @param {string[] | undefined} files per-file: лише ці файли; undefined: весь проєкт (--full)
* @param {string} [cwd] корінь репо
* @param {{ readOnly?: boolean }} [opts] readOnly → без `--fix` (нуль мутацій)
* @returns {Promise<number>} 0 — OK, ≠0 — порушення
*/
export function lint(files, cwd = process.cwd()) {
export function lint(files, cwd = process.cwd(), opts = {}) {
const readOnly = opts.readOnly === true
if (files === undefined) {
return Promise.resolve(lintFullProject(cwd))
return Promise.resolve(lintFullProject(cwd, readOnly))
}
const js = filterJsFiles(files)
if (js.length === 0) return Promise.resolve(0)
return Promise.resolve(lintChangedClassified(js, cwd))
return Promise.resolve(lintChangedClassified(js, cwd, readOnly))
}

@@ -1,1 +0,1 @@

{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "quick" }
{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "per-file" }
---
docgen:
source: npm/rules/npm-module/js/rule_meta.mjs
crc: fa29bd00
crc: 8262678c
score: 100

@@ -6,0 +6,0 @@ ---

@@ -6,3 +6,3 @@ /** @see ./docs/rule_meta.md */

import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
import { parseRuleAutoSpec, parseRuleLintSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs'

@@ -41,4 +41,4 @@

if (raw.lint === undefined) return true
if (parseRuleLintPhase(raw.lint) === null) {
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
if (parseRuleLintSpec(raw.lint) === null) {
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "per-file"|"full")`)
return false

@@ -45,0 +45,0 @@ }

@@ -1,1 +0,1 @@

{ "auto": { "glob": "**/*.rego" }, "lint": "ci" }
{ "auto": { "glob": "**/*.rego" }, "lint": "full" }

@@ -1,1 +0,1 @@

{ "auto": "завжди", "lint": "ci" }
{ "auto": "завжди", "lint": "per-file" }
---
docgen:
source: npm/rules/style-lint/js/lint.mjs
crc: 94e067b3
crc: 2013a66b
score: 100

@@ -6,0 +6,0 @@ ---

@@ -15,8 +15,9 @@ /** @see ./docs/lint.md */

/**
* @param {string[] | undefined} files quick: ці файли; undefined: весь проєкт
* @param {string[] | undefined} files per-file: ці файли; undefined: весь проєкт (--full)
* @param {string} [cwd] корінь
* @param {{ readOnly?: boolean }} [opts] readOnly → без `--fix` (детект, нуль мутацій)
* @returns {Promise<number>} exit code
*/
export function lint(files, cwd = process.cwd()) {
const args = ['stylelint', '--fix']
export function lint(files, cwd = process.cwd(), opts = {}) {
const args = opts.readOnly === true ? ['stylelint'] : ['stylelint', '--fix']
if (files === undefined) {

@@ -23,0 +24,0 @@ args.push('**/*.{css,scss,vue}')

@@ -1,1 +0,1 @@

{ "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "quick" }
{ "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "per-file" }
---
docgen:
source: npm/rules/text/js/lint.mjs
crc: 4ee054a0
crc: 49aab7ce
score: 100

@@ -6,0 +6,0 @@ ---

/**
* Ci-крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
* Крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
*/

@@ -8,6 +8,8 @@ import { runLintTextCli } from '../lint/lint.mjs'

* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
* @param {string} [_cwd] корінь (ігнорується — CLI працює від process.cwd())
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій)
* @returns {Promise<number>} exit code
*/
export function lint(_files) {
return runLintTextCli()
export function lint(_files, _cwd, opts = {}) {
return runLintTextCli({ readOnly: opts.readOnly === true })
}
---
docgen:
source: npm/rules/text/lint/lint.mjs
crc: 05f3f108
crc: bdaef0f8
---

@@ -6,0 +6,0 @@

---
docgen:
source: npm/rules/text/lint/run-dotenv-linter.mjs
crc: 8bb94af4
crc: 4719ac66
---

@@ -6,0 +6,0 @@

---
docgen:
source: npm/rules/text/lint/run-shellcheck.mjs
crc: e6fa8c23
crc: 6b2daaa8
---

@@ -6,0 +6,0 @@

@@ -98,5 +98,6 @@ /**

* Внутрішні кроки `lint-text` без локу.
* @param {boolean} [readOnly] true → лише детект без авто-фіксу (нуль мутацій — CI/pre-commit)
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
*/
function runLintTextSteps() {
function runLintTextSteps(readOnly = false) {
// Auto-install: throws on failure → propagates as exit 1 from runStandardLint

@@ -106,4 +107,4 @@ ensureTool('shellcheck')

// patch is hint-only (system tool)
if (!preflight(PATCH_PREFLIGHT)) return 1
// patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight.
if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1

@@ -113,11 +114,12 @@ const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.'])

console.log('\n▶ shellcheck (авто-фікс + фінальна перевірка *.sh)')
const shellcheckCode = runShellcheckText()
console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`)
const shellcheckCode = runShellcheckText(process.cwd(), readOnly)
if (shellcheckCode !== 0) return shellcheckCode
console.log('\n▶ dotenv-linter (авто-фікс + фінальна перевірка .env*)')
const dotenvCode = runDotenvLinter()
console.log(`\n▶ dotenv-linter (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} .env*)`)
const dotenvCode = runDotenvLinter(process.cwd(), readOnly)
if (dotenvCode !== 0) return dotenvCode
const markdownlintCode = runLintStep('markdownlint', 'bunx', ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc'])
const mdArgs = readOnly ? ['markdownlint-cli2', '**/*.md', '**/*.mdc'] : ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc']
const markdownlintCode = runLintStep('markdownlint', 'bunx', mdArgs)
if (markdownlintCode !== 0) return markdownlintCode

@@ -131,4 +133,6 @@

* Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу
* @returns {Promise<number>} код виходу
*/
export const runLintTextCli = () => runStandardLint(import.meta.dirname, () => runLintTextSteps())
export const runLintTextCli = (opts = {}) =>
runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true))

@@ -55,5 +55,6 @@ /**

* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
* @param {boolean} [readOnly] true → пропустити авто-фікс (`fix`), лише `check` (нуль мутацій)
* @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
*/
export function runDotenvLinter(cwd = process.cwd()) {
export function runDotenvLinter(cwd = process.cwd(), readOnly = false) {
const root = resolve(cwd)

@@ -67,11 +68,13 @@ const bin = resolveCmd('dotenv-linter')

const exclude = buildExcludeArgs()
const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], {
cwd: root,
encoding: 'utf8',
env: process.env,
stdio: ['ignore', 'pipe', 'pipe']
})
if (fixRun.error) {
process.stderr.write(`${fixRun.error.message}\n`)
return 1
if (!readOnly) {
const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], {
cwd: root,
encoding: 'utf8',
env: process.env,
stdio: ['ignore', 'pipe', 'pipe']
})
if (fixRun.error) {
process.stderr.write(`${fixRun.error.message}\n`)
return 1
}
}

@@ -78,0 +81,0 @@

@@ -99,5 +99,6 @@ /**

* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
* @param {boolean} [readOnly] true → пропустити авто-фікс (diff+patch), лише фінальна перевірка
* @returns {number} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
*/
export function runShellcheckText(cwd = process.cwd()) {
export function runShellcheckText(cwd = process.cwd(), readOnly = false) {
const root = resolve(cwd)

@@ -109,4 +110,5 @@ const shellcheck = resolveCmd('shellcheck')

}
const patchBin = resolveCmd('patch')
if (!patchBin) {
// patch потрібен лише для авто-фіксу (diff+patch); у read-only його відсутність не блокує детект.
const patchBin = readOnly ? null : resolveCmd('patch')
if (!readOnly && !patchBin) {
printPatchInstallHints()

@@ -121,5 +123,7 @@ return 1

for (const rel of files) {
const fixCode = autofixOneFile(shellcheck, patchBin, root, rel)
if (fixCode !== 0) return fixCode
if (!readOnly) {
for (const rel of files) {
const fixCode = autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel)
if (fixCode !== 0) return fixCode
}
}

@@ -126,0 +130,0 @@

@@ -1,1 +0,1 @@

{ "auto": "завжди", "lint": "ci" }
{ "auto": "завжди", "lint": "per-file" }
---
docgen:
source: npm/scripts/lint-cli.mjs
crc: d4a7562d
crc: 9e0a12b9
score: 100

@@ -6,0 +6,0 @@ ---

---
docgen:
source: npm/scripts/lib/rule-meta.mjs
crc: 4475d5ff
crc: fa5ca866
---

@@ -6,0 +6,0 @@

@@ -51,12 +51,16 @@ /**

/** Допустимі фази lint. */
const LINT_PHASES = new Set(['quick', 'ci'])
/** Допустимі значення `meta.json.lint` (вісь scope: чи детектор дробиться на changed-set). */
const LINT_SCOPES = new Set(['per-file', 'full'])
/**
* Нормалізує значення `meta.json.lint` у фазу lint.
* Нормалізує значення `meta.json.lint` у scope детектора.
* - `"per-file"` — детектор декомпозується на змінені файли (дельта vs origin);
* - `"full"` — нероздільно крос-файловий (лише `--full` / CI).
* Об'єктна форма `{scope, ci}` скасована: CI=`--read-only --full` ганяє все повністю,
* тож per-rule CI-override не потрібен (spec 2026-06-14-lint-rule-consolidation §3-А).
* @param {unknown} value значення поля `lint`
* @returns {'quick' | 'ci' | null} фаза або `null` (відсутнє/невалідне = не lint-крок)
* @returns {'per-file' | 'full' | null} scope або `null` (відсутнє/невалідне = не lint-крок)
*/
export function parseRuleLintPhase(value) {
return typeof value === 'string' && LINT_PHASES.has(value) ? /** @type {'quick'|'ci'} */ (value) : null
export function parseRuleLintSpec(value) {
return typeof value === 'string' && LINT_SCOPES.has(value) ? /** @type {'per-file'|'full'} */ (value) : null
}

@@ -63,0 +67,0 @@

/**
* Оркестратор `n-cursor lint` (quick) / `n-cursor lint-ci` (full).
* Оркестратор `n-cursor lint` — дві ортогональні осі (spec 2026-06-14-lint-rule-consolidation
* + компаньйон 2026-06-14-lint-orchestrator-fix-readonly-unification):
* - **scope** (`--full`): default = дельта vs origin (лише `per-file` правила);
* `--full` = весь репо (`per-file` ∪ `full` правила);
* - **behavior** (`--read-only`): default = fix; `--read-only` = лише детект без мутацій.
*
* Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`quick`|`ci`),
* послідовно (заборона паралельного eslint) викликає `rules/<id>/js/lint.mjs`:
* - quick: `lint(changedFiles)` — лише змінені файли (git diff HEAD + untracked);
* - ci: `lint(undefined)` — весь проєкт.
* Порядок правил — алфавітний; ci-набір = quick ∪ ci. Fail-fast: перший ненульовий код спиняє.
* Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`per-file`|`full`),
* викликає `rules/<id>/js/lint.mjs` → `lint(files, cwd, { readOnly })`:
* - default scope: `files` = змінені відносно origin (`collectChangedFilesSince`);
* - `--full`: `files = undefined` — весь проєкт.
* Порядок правил — алфавітний. Fail-fast: перший ненульовий код спиняє.
*/

@@ -15,4 +19,4 @@ import { existsSync, readdirSync } from 'node:fs'

import { parseRuleLintPhase, readRuleMetaRaw } from './lib/rule-meta.mjs'
import { collectChangedFiles } from './lib/changed-files.mjs'
import { parseRuleLintSpec, readRuleMetaRaw } from './lib/rule-meta.mjs'
import { collectChangedFilesSince, resolveChangedBase } from './lib/changed-files.mjs'

@@ -23,12 +27,12 @@ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))

/**
* Вибирає id правил для фази, алфавітно.
* Вибирає id правил для контексту, алфавітно.
* @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт
* @param {'quick'|'ci'} phase цільова фаза (quick → лише quick; ci → quick+ci)
* @param {boolean} full `false` → лише `per-file` правила; `true` → усі (`per-file` ∪ `full`)
* @returns {string[]} відсортовані id
*/
export function selectLintRules(metaById, phase) {
export function selectLintRules(metaById, full) {
const out = []
for (const [id, raw] of Object.entries(metaById)) {
const p = parseRuleLintPhase(raw?.lint)
if (p === 'quick' || (phase === 'ci' && p === 'ci')) out.push(id)
const scope = parseRuleLintSpec(raw?.lint)
if (scope === 'per-file' || (full && scope === 'full')) out.push(id)
}

@@ -57,7 +61,10 @@ return out.toSorted((a, b) => a.localeCompare(b))

* Запускає lint-оркестрацію.
* @param {{ ci?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
* @param {{ full?: boolean, readOnly?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
* - `full` — весь репо (`true`) проти дельти vs origin (`false`, default);
* - `readOnly` — лише детект без мутацій (`true`) проти fix (`false`, default).
* @returns {Promise<number>} exit code
*/
export async function runLint(opts = {}) {
const ci = opts.ci === true
const full = opts.full === true
const readOnly = opts.readOnly === true
const cwd = opts.cwd ?? processCwd()

@@ -67,9 +74,10 @@ const rulesDir = opts.rulesDir ?? RULES_DIR

const changed = ci ? undefined : collectChangedFiles(cwd)
if (!ci && changed.length === 0) {
log('\nℹ️ lint: немає змінених файлів — нічого перевіряти.\n')
// Default scope — дельта vs origin (merge-base main/origin/main); `--full` — весь репо.
const changed = full ? undefined : collectChangedFilesSince(resolveChangedBase(cwd), cwd)
if (!full && changed.length === 0) {
log('\nℹ️ lint: немає змінених файлів відносно origin — нічого перевіряти.\n')
return 0
}
const ids = selectLintRules(readAllMeta(rulesDir), ci ? 'ci' : 'quick')
const ids = selectLintRules(readAllMeta(rulesDir), full)
for (const id of ids) {

@@ -82,3 +90,3 @@ const lintPath = join(rulesDir, id, 'js', 'lint.mjs')

const mod = await import(lintPath)
const code = await mod.lint(changed, cwd)
const code = await mod.lint(changed, cwd, { readOnly })
if (code !== 0) return code

@@ -85,0 +93,0 @@ }

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet