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
403
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
9.2.0
to
9.3.0
+104
rules/text/lint/cspell-fix.mjs
/**
* cspell у ланцюжку lint-text із omlx-автофіксом (point 4 спеки).
*
* cspell не має нативного `--fix`. У fix-режимі: детект (захоплення виводу) → групування
* знахідок по файлах → per-file omlx-фікс справжніх одруків (`llmLintFix`) → re-detect.
* У read-only: лише детект (нуль мутацій). Валідні терміни omlx лишає — їх ловить повторний
* cspell (далі — у словник `@nitra/cspell-dict`).
*/
import { spawnSync } from 'node:child_process'
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
import { llmLintFix } from '../../../scripts/lib/fix/llm-lint-fix.mjs'
/** Рядок cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */
const CSPELL_LINE_RE = /^(.+?):\d+:\d+\s+-\s+Unknown word/u
/** Максимум файлів під omlx-фікс за прогін (без тихого обрізання — логуємо надлишок). */
const MAX_FIX_FILES = 25
/**
* Запускає `cspell .` із захопленням виводу.
* @param {string} cwd корінь
* @param {string} bin шлях до cspell (npx/локальний)
* @returns {{ code:number, out:string }} код + обʼєднаний stdout/stderr
*/
function detectCspell(cwd, bin) {
const r = spawnSync(bin, ['cspell', '.'], { cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, env: process.env })
return { code: typeof r.status === 'number' ? r.status : 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` }
}
/**
* Групує cspell-знахідки за файлом.
* @param {string} out вивід cspell
* @returns {Map<string, string[]>} файл → рядки знахідок
*/
export function groupFindingsByFile(out) {
/** @type {Map<string, string[]>} */
const byFile = new Map()
for (const line of out.split('\n')) {
const m = CSPELL_LINE_RE.exec(line.trim())
if (!m) continue
const file = m[1]
if (!byFile.has(file)) byFile.set(file, [])
byFile.get(file).push(line.trim())
}
return byFile
}
const CSPELL_INSTRUCTION = [
'Correct genuine spelling typos in the file(s).',
'Each flagged "Unknown word" is listed below.',
'ONLY fix obvious misspellings of real words.',
'If a flagged token is a valid identifier, technical term, abbreviation, proper noun, URL,',
'or an intentional non-English word, leave it UNCHANGED (it will be added to the dictionary).',
'Preserve all code, formatting, and unrelated text exactly.'
].join(' ')
/**
* cspell-крок lint-text з omlx-автофіксом.
* @param {string} [cwd] корінь
* @param {boolean} [readOnly] true → лише детект (нуль мутацій)
* @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
*/
export function runCspellText(cwd = process.cwd(), readOnly = false) {
const bin = resolveCmd('npx')
if (!bin) {
process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
return 1
}
const first = detectCspell(cwd, bin)
if (first.code === 0) return 0
if (readOnly) {
process.stdout.write(first.out)
return first.code
}
// Fix-режим: omlx по файлах зі справжніми одруками.
const byFile = groupFindingsByFile(first.out)
const files = [...byFile.keys()]
if (files.length === 0) {
process.stdout.write(first.out)
return first.code
}
const targets = files.slice(0, MAX_FIX_FILES)
if (files.length > MAX_FIX_FILES) {
process.stdout.write(`ℹ️ cspell: omlx-фікс перших ${MAX_FIX_FILES}/${files.length} файлів (решта — наступний прогін)\n`)
}
for (const file of targets) {
const res = llmLintFix({
tool: 'cspell',
instruction: CSPELL_INSTRUCTION,
findings: byFile.get(file).join('\n'),
filePaths: [file],
projectRoot: cwd
})
process.stdout.write(res.ok ? ` ⚡ cspell omlx-фікс: ${file}\n` : ` ⚠️ cspell omlx-фікс пропущено (${file}): ${res.error}\n`)
}
// Re-detect: що лишилось (валідні терміни → у словник).
const second = detectCspell(cwd, bin)
if (second.code !== 0) process.stdout.write(second.out)
return second.code
}
/**
* Спільне ядро LLM-фіксу: парс відповіді `{changes:[{path,content}]}`, читання файлів
* під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
* `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
/**
* Парсить JSON-відповідь моделі: прямий JSON → ```json-блок``` → перший `{…}`-блок.
* @param {string} text сирий текст відповіді
* @returns {{ changes?: Array<{path:string,content:string}>, error?: string } | null} патч або null
*/
export function parseChangesResponse(text) {
try {
return JSON.parse(text)
} catch {
/* fallthrough */
}
const block = text.match(JSON_CODE_BLOCK_RE)
if (block) {
try {
return JSON.parse(block[1].trim())
} catch {
/* fallthrough */
}
}
const start = text.indexOf('{')
const end = text.lastIndexOf('}')
if (start !== -1 && end > start) {
try {
return JSON.parse(text.slice(start, end + 1))
} catch {
/* fallthrough */
}
}
return null
}
/**
* Читає існуючі файли за відносними шляхами у форму `{path, content}` (для prompt).
* @param {string[]} filePaths відносні шляхи від кореня
* @param {string} projectRoot абсолютний корінь
* @returns {Array<{path:string, content:string}>} наявні файли з вмістом
*/
export function readFilesForFix(filePaths, projectRoot) {
return filePaths
.map(p => {
const abs = join(projectRoot, p)
if (!existsSync(abs)) return null
try {
return { path: p, content: readFileSync(abs, 'utf8') }
} catch {
return null
}
})
.filter(Boolean)
}
/**
* Застосовує `changes` до ФС (повний вміст файлу, не diff).
* @param {Array<{path:string, content:string}>} changes зміни
* @param {string} projectRoot абсолютний корінь
* @returns {{ ok: boolean, error?: string }} статус
*/
export function applyChanges(changes, projectRoot) {
for (const change of changes) {
if (!change.path || typeof change.content !== 'string') continue
try {
writeFileSync(join(projectRoot, change.path), change.content, 'utf8')
} catch (error) {
return { ok: false, error: `write ${change.path}: ${error.message}` }
}
}
return { ok: true }
}
/**
* Per-tool omlx-фікс лінтер-знахідок (point 4 спеки lint-orchestrator-fix-readonly).
*
* Для detect-only тулів без нативного `--fix` (cspell, knip, actionlint, v8r тощо): читає
* уражені файли, просить omlx виправити за tool-специфічною інструкцією, застосовує `{changes}`.
* Re-detect (перевірка, що знахідка закрита) — на стороні caller (convergence-патерн).
*
* Маршрут моделі — через `callLlm` за префіксом: `omlx/<model>` → локальний HTTP (дефолт
* `resolveModel('min')`); cloud — фолбек каскаду. Парс/застосування — спільне ядро `llm-fix-apply`.
*/
import { env } from 'node:process'
import { resolveModel } from '../../../lib/models.mjs'
import { callLlm } from '../../../lib/llm.mjs'
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'
/** Дефолтний локальний тир (omlx); env `N_CURSOR_FIX_MODEL` перекриває. */
const DEFAULT_MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
/**
* Будує prompt для omlx: tool-інструкція + знахідки + повний вміст файлів.
* @param {string} tool назва тула (cspell/knip/…)
* @param {string} instruction що саме виправити (tool-специфічно)
* @param {string} findings сирий вивід тула (знахідки)
* @param {Array<{path:string, content:string}>} files файли під фікс
* @returns {string} prompt
*/
function buildLintFixPrompt(tool, instruction, findings, files) {
const filesBlock = files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
return [
`You fix ${tool} lint findings. Return ONLY valid JSON — no explanation, no markdown.`,
``,
`Task: ${instruction}`,
``,
`${tool} findings:`,
findings,
``,
`Current file contents:`,
filesBlock,
``,
`Return JSON with this exact shape:`,
`{"changes":[{"path":"relative/path","content":"full corrected file content"}]}`,
``,
`Rules:`,
`- "path" is relative to the project root (use the path from the <file> tag)`,
`- "content" is the COMPLETE new file content (not a diff)`,
`- Only include files that actually need to change; preserve everything unrelated verbatim`,
`- If nothing should be auto-fixed, return {"changes":[],"error":"reason"}`
].join('\n')
}
/**
* Виправляє лінтер-знахідки через omlx і застосовує зміни.
* @param {{ tool:string, instruction:string, findings:string, filePaths:string[], projectRoot:string, model?:string }} opts параметри
* @returns {{ ok:boolean, error?:string, fixed:string[] }} статус + список змінених шляхів
*/
export function llmLintFix({ tool, instruction, findings, filePaths, projectRoot, model }) {
const m = model ?? DEFAULT_MODEL
const files = readFilesForFix(filePaths, projectRoot)
if (files.length === 0) return { ok: false, error: 'no readable files to fix', fixed: [] }
let text
try {
text = callLlm([{ role: 'user', content: buildLintFixPrompt(tool, instruction, findings, files) }], m, {
timeoutMs: 120_000,
caller: `lint:${tool}`
})
} catch (error) {
return { ok: false, error: String(error.message), fixed: [] }
}
const parsed = parseChangesResponse(text)
if (!parsed) return { ok: false, error: `cannot parse omlx response: ${String(text).slice(0, 200)}`, fixed: [] }
if (parsed.error) return { ok: false, error: parsed.error, fixed: [] }
const changes = (parsed.changes ?? []).filter(c => c.path && typeof c.content === 'string')
if (changes.length === 0) return { ok: false, error: 'omlx returned no changes', fixed: [] }
const applied = applyChanges(changes, projectRoot)
if (!applied.ok) return { ok: false, error: applied.error, fixed: [] }
return { ok: true, fixed: changes.map(c => c.path) }
}
+1
-1
{
"name": "@nitra/cursor",
"version": "9.2.0",
"version": "9.3.0",
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",

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

@@ -27,2 +27,3 @@ /**

import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
import { runCspellText } from './cspell-fix.mjs'
import { runDotenvLinter } from './run-dotenv-linter.mjs'

@@ -110,3 +111,4 @@ import { runShellcheckText } from './run-shellcheck.mjs'

const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.'])
console.log(`\n▶ cspell (${readOnly ? 'перевірка' : 'omlx-автофікс одруків + перевірка'})`)
const cspellCode = runCspellText(process.cwd(), readOnly)
if (cspellCode !== 0) return cspellCode

@@ -113,0 +115,0 @@

/** @see ./docs/llm-worker.md */
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

@@ -8,2 +8,3 @@ import { env } from 'node:process'

import { callLlm } from '../../../lib/llm.mjs'
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'

@@ -15,3 +16,2 @@ // Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).

const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
const API_KEY_RE = /api key/i

@@ -119,40 +119,2 @@

/**
* Парсить JSON-відповідь від моделі.
* Модель може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
* @param {string} text сирий текст відповіді
* @returns {{ changes: Array<{path:string,content:string}>, error?: string } | null} розпарсений патч або null
*/
function parseResponse(text) {
// Спроба 1: прямий JSON
try {
return JSON.parse(text)
} catch {
/* fallthrough */
}
// Спроба 2: витягти з ```json ... ```
const m = text.match(JSON_CODE_BLOCK_RE)
if (m) {
try {
return JSON.parse(m[1].trim())
} catch {
/* fallthrough */
}
}
// Спроба 3: перший { ... } блок
const start = text.indexOf('{')
const end = text.lastIndexOf('}')
if (start !== -1 && end > start) {
try {
return JSON.parse(text.slice(start, end + 1))
} catch {
/* fallthrough */
}
}
return null
}
/**
* LLM-worker: виправляє одне rule-порушення через pi (C1 pattern).

@@ -173,14 +135,3 @@ * @param {string} ruleId ID правила

// 2. Витягуємо файли з violation output і читаємо їх
const filePaths = extractFilePaths(violationOutput)
const files = filePaths
.map(p => {
const abs = join(projectRoot, p)
if (!existsSync(abs)) return null
try {
return { path: p, content: readFileSync(abs, 'utf8') }
} catch {
return null
}
})
.filter(Boolean)
const files = readFilesForFix(extractFilePaths(violationOutput), projectRoot)

@@ -195,3 +146,3 @@ // 3. Будуємо prompt і викликаємо модель

// 4. Парсимо відповідь
const parsed = parseResponse(text)
const parsed = parseChangesResponse(text)
if (!parsed) return { ok: false, error: `cannot parse pi response: ${text.slice(0, 200)}` }

@@ -204,13 +155,3 @@ if (parsed.error) return { ok: false, error: parsed.error }

// 5. Застосовуємо зміни
for (const change of changes) {
if (!change.path || typeof change.content !== 'string') continue
const abs = join(projectRoot, change.path)
try {
writeFileSync(abs, change.content, 'utf8')
} catch (error) {
return { ok: false, error: `write ${change.path}: ${error.message}` }
}
}
return { ok: true }
return applyChanges(changes, projectRoot)
}

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