@nitra/cursor 1.13.57 → 1.13.60
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/.claude-template/hooks/capture-decisions.sh +6 -2
- package/CHANGELOG.md +14 -1
- package/bin/n-cursor.js +4 -48
- package/package.json +1 -1
- package/rules/bun/fix/layout/check.mjs +17 -6
- package/rules/feedback/feedback.mdc +63 -0
- package/rules/js-bun-db/fix/safety/check.mjs +9 -3
- package/rules/js-bun-db/js-bun-db.mdc +2 -2
- package/rules/security/fix/sample_secret/check.mjs +105 -0
- package/rules/security/security.mdc +17 -2
- package/scripts/utils/bun-sql-scan.mjs +6 -5
- package/scripts/utils/generated-markdown.mjs +71 -0
|
@@ -44,9 +44,12 @@ fi
|
|
|
44
44
|
# Extract role + text + thinking + tool_use names from JSONL transcript.
|
|
45
45
|
# We keep reasoning/decisions visible to the analyzer but drop large tool outputs.
|
|
46
46
|
TRANSCRIPT=$(jq -r '
|
|
47
|
-
select(
|
|
47
|
+
select(
|
|
48
|
+
.type == "user" or .type == "assistant"
|
|
49
|
+
or .role == "user" or .role == "assistant"
|
|
50
|
+
)
|
|
48
51
|
| .message as $m
|
|
49
|
-
| ($m.role // .type) as $role
|
|
52
|
+
| ($m.role // .role // .type) as $role
|
|
50
53
|
| ($m.content
|
|
51
54
|
| if type == "string" then .
|
|
52
55
|
else (
|
|
@@ -79,6 +82,7 @@ if (( ${#TRANSCRIPT} > MAX_CHARS )); then
|
|
|
79
82
|
fi
|
|
80
83
|
|
|
81
84
|
if [[ -z "$TRANSCRIPT" ]]; then
|
|
85
|
+
log " → empty transcript after jq (Claude Code: .type; Cursor Agent: .role)"
|
|
82
86
|
exit 0
|
|
83
87
|
fi
|
|
84
88
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +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.13.60] - 2026-05-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Генерація **`AGENTS.md`** і **`CLAUDE.md`** при `npx @nitra/cursor`: Mustache-секції більше не вставляють порожній рядок між кожним пунктом списку (MD012), а фінальний markdown згортає зайві `\n\n\n` на стиках секцій — не потрібен окремий `lint-text` лише заради зачистки згенерованих файлів. Зачеплено: [generated-markdown.mjs](scripts/utils/generated-markdown.mjs) (`expandMustacheSection` — trim inner + `join('\n')`, `collapseMultipleBlankLines`, `formatGeneratedMarkdownLines`), [n-cursor.js](bin/n-cursor.js) (імпорт утиліт замість inline-логіки), [generated-markdown.test.mjs](scripts/utils/generated-markdown.test.mjs).
|
|
12
|
+
|
|
13
|
+
## [1.13.59] - 2026-05-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Нове `alwaysApply`-правило **`feedback`** ([feedback.mdc](rules/feedback/feedback.mdc)) — ефемерний канал зворотного звʼязку до пакета `@nitra/cursor`. Виконуючи будь-який скіл пакета (`n-lint`, `n-fix`, `n-taze`, `n-adr-normalize`, `n-llm-patch`, `n-publish-telegram`, `mdc-check`), агент проходить крізь `.cursor/rules/`, `SKILL.md` і `npx @nitra/cursor check` — і бачить «тертя»: неоднозначні інструкції, відсутні `check-*.mjs`, false positive, порушення без автофіксу, повторювані патерни. Правило вимагає наприкінці скілу, **після** основного резюме, додати у відповідь чату секцію `## 🔧 Покращення @nitra/cursor` з пунктами за схемою `target` (`rule`/`skill`/`check`) · `id` · `kind` (`ambiguous-doc`/`missing-check`/`false-positive`/`no-autofix`/`recurring-pattern`) · `evidence` · `suggestion`. Резюме **навмисно ефемерне** — живе лише у відповіді чату: правило забороняє запис файлів/чернеток, GitHub issue/PR і редагування самого пакета; розробник, читаючи відповідь, сам вирішує, чи переносити пункт у пакет. Якщо тертя не було — секція повністю пропускається. Правило чисто документаційне (як `ci4`), `check-*.mjs` не має, бо поведінка агента програмно не верифікується. Зачеплено: новий каталог [rules/feedback/](rules/feedback/) з `feedback.mdc` (`version: '1.0'`), додано `"feedback"` у `rules` кореневого `.n-cursor.json` — після синку правило копіюється як `.cursor/rules/n-feedback.mdc` і потрапляє в `AGENTS.md`.
|
|
18
|
+
- `check security`: новий concern **`security.sample_secret`** — placeholder фейкових credential-значень у прикладних файлах має бути `sample-secret`, а не bare `secret`. Причина: `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog і відсіюється сканером гарантовано та незалежно від версії; bare `secret` наразі ігнорується лише тому, що випадково присутнє у словнику `fp_words.txt` — крихка поведінка, що залежить від версії інструмента. [check.mjs](rules/security/fix/sample_secret/check.mjs) обходить дерево, відбирає прикладні файли (basename із суфіксом `.example`/`.sample`/`.template`/`.dist` чи infix `.example.`/`.sample.`/`.template.`, а також усе всередині каталогів `fixtures`/`fixture`/`__fixtures__`) і порядково шукає `secret` у позиції значення — одразу після `=`, `:` або `=>` з опційними лапками; імена ключів (`client_secret`, `JWT_SECRET`) не чіпаються, бо матч прив'язаний до значення. Решта файлів не сканується — там `secret` майже завжди частина реального коду. Скан текстовий (regex, не AST/Rego): прикладні файли — різнорідні конфіги (`.env`, YAML, JSON, TOML, plain `.dist`) без єдиного AST, а відбір файлів потребує обходу дерева. Зачеплено: [check.mjs](rules/security/fix/sample_secret/check.mjs) і [check.test.mjs](rules/security/fix/sample_secret/check.test.mjs) (новий concern + 9 тестів), [security.mdc](rules/security/security.mdc) (нова секція «Placeholder для секретів — `sample-secret`» та секція «Перевірка»). Bump `security.mdc` `2.0` → `2.1`.
|
|
19
|
+
|
|
7
20
|
## [1.13.57] - 2026-05-19
|
|
8
21
|
|
|
9
22
|
### Changed
|
|
10
23
|
|
|
11
|
-
- `check js-bun-db`: новий **hard fail** на `sql.unsafe(template_literal_with_interpolation)` — будь-який виклик з template-літералом, що містить `${...}`-інтерполяцію, тепер падає **навіть з маркером** `// allow-unsafe`. Причина: шаблонна підстановка `${name}` у `sql.unsafe`-рядок не екранує identifier'ів (reserved words, спецсимволи, пробіли в імені) і не біндить значень; такий код виглядає звично через знайому tagged-template-форму, але насправді робить просту строкову конкатенацію без жодних гарантій. Канон — зібрати `text` окремо: identifiers через `@scaleleap/pg-format` `format('%I', name)`, values як позиційні `$N` + другий аргумент `sql.unsafe(text, [params])`. Раніше дозволений приклад `sql.unsafe(\\\`CREATE TABLE \\\${TABLE} (id int)\\\`)
|
|
24
|
+
- `check js-bun-db`: новий **hard fail** на `sql.unsafe(template_literal_with_interpolation)` — будь-який виклик з template-літералом, що містить `${...}`-інтерполяцію, тепер падає **навіть з маркером** `// allow-unsafe`. Причина: шаблонна підстановка `${name}` у `sql.unsafe`-рядок не екранує identifier'ів (reserved words, спецсимволи, пробіли в імені) і не біндить значень; такий код виглядає звично через знайому tagged-template-форму, але насправді робить просту строкову конкатенацію без жодних гарантій. Канон — зібрати `text` окремо: identifiers через `@scaleleap/pg-format` `format('%I', name)`, values як позиційні `$N` + другий аргумент `sql.unsafe(text, [params])`. Раніше дозволений приклад `sql.unsafe(\\\`CREATE TABLE \\\${TABLE} (id int)\\\`)`з marker'ом тепер fail — переписати через`format('CREATE TABLE %I (id int)', TABLE)`. Не зачепило:`sql.unsafe('SELECT 1')`(статичний рядок),`sql.unsafe(\\\`SELECT 1\\\`)`(template без інтерполяції),`sql.unsafe(text, [params])`зі змінною`text`. Зачеплено: [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) (новий експорт`findBunSqlUnsafeWithInterpolatedTemplateInText`, що флагає лише`obj.unsafe(TemplateLiteral)`з`expressions.length > 0`), [check.mjs](rules/js-bun-db/fix/safety/check.mjs) (новий лічильник`unsafeTemplateInterp`+ окреме повідомлення з порадою на`@scaleleap/pg-format`), [check.test.mjs](rules/js-bun-db/fix/safety/check.test.mjs) (попередній DDL-тест переписано на безпечний`format('%I', ...)`-варіант, додано **негативний** тест на template-interp + marker і **позитивний** тест на статичний template без інтерполяції), [js-bun-db.mdc](rules/js-bun-db/js-bun-db.mdc) (нова підсекція «sql.unsafe з template-літералом і ${...}-інтерполяцією — заборонено навіть з маркером» зі зразками поганого/гарного коду; основний приклад DDL у секції unsafe-allowlist переписано на`format`+ готовий`text`). Bump`js-bun-db.mdc` `1.10`→`1.11`.
|
|
12
25
|
|
|
13
26
|
## [1.13.56] - 2026-05-19
|
|
14
27
|
|
package/bin/n-cursor.js
CHANGED
|
@@ -67,6 +67,7 @@ import { cwd, env } from 'node:process'
|
|
|
67
67
|
import { fileURLToPath } from 'node:url'
|
|
68
68
|
|
|
69
69
|
import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
|
|
70
|
+
import { formatGeneratedMarkdownLines, renderAgentsTemplate } from '../scripts/utils/generated-markdown.mjs'
|
|
70
71
|
import { inlineTemplateLinks } from '../scripts/utils/inline-template-links.mjs'
|
|
71
72
|
import {
|
|
72
73
|
detectAutoRules,
|
|
@@ -481,50 +482,6 @@ function formatClaudeCommandFrontmatter(descriptionRaw) {
|
|
|
481
482
|
return `---\ndescription: >-\n ${text}\n---\n\n`
|
|
482
483
|
}
|
|
483
484
|
|
|
484
|
-
/**
|
|
485
|
-
* Розгортає в шаблоні блок Mustache {{#section}} … {{/section}} для масиву елементів
|
|
486
|
-
* @param {string} template вихідний текст шаблону
|
|
487
|
-
* @param {string} section ім'я секції (наприклад services)
|
|
488
|
-
* @param {Record<string, string>[]} items елементи для повторення тіла секції
|
|
489
|
-
* @param {string} prop ключ поля для підстановки замість {{prop}}
|
|
490
|
-
* @returns {string} текст після розгортання усіх входжень блоку
|
|
491
|
-
*/
|
|
492
|
-
function expandMustacheSection(template, section, items, prop) {
|
|
493
|
-
const open = `{{#${section}}}`
|
|
494
|
-
const close = `{{/${section}}}`
|
|
495
|
-
const placeholder = `{{${prop}}}`
|
|
496
|
-
let result = template
|
|
497
|
-
let start = result.indexOf(open)
|
|
498
|
-
let end = result.indexOf(close)
|
|
499
|
-
while (start !== -1 && end !== -1 && end > start) {
|
|
500
|
-
const inner = result.slice(start + open.length, end)
|
|
501
|
-
const rendered = items.map(item => inner.split(placeholder).join(String(item[prop]))).join('')
|
|
502
|
-
result = result.slice(0, start) + rendered + result.slice(end + close.length)
|
|
503
|
-
start = result.indexOf(open)
|
|
504
|
-
end = result.indexOf(close)
|
|
505
|
-
}
|
|
506
|
-
return result
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Підставляє у вміст AGENTS.template.md список шляхів до файлів правил, skills і команд з package.json
|
|
511
|
-
* @param {string} templateText вміст AGENTS.template.md
|
|
512
|
-
* @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
|
|
513
|
-
* @param {{ name: string }[]} skillItems рядки для секції Skills
|
|
514
|
-
* @param {{ name: string }[]} commandItems рядки для секції commands
|
|
515
|
-
* @returns {string} готовий markdown для AGENTS.md
|
|
516
|
-
*/
|
|
517
|
-
function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems) {
|
|
518
|
-
let result = templateText
|
|
519
|
-
const serviceItems = mdcBasenames.map(mdcName => ({
|
|
520
|
-
name: `- ${RULES_DIR}/${mdcName}`
|
|
521
|
-
}))
|
|
522
|
-
result = expandMustacheSection(result, 'services', serviceItems, 'name')
|
|
523
|
-
result = expandMustacheSection(result, 'skills', skillItems, 'name')
|
|
524
|
-
result = expandMustacheSection(result, 'commands', commandItems, 'name')
|
|
525
|
-
return result
|
|
526
|
-
}
|
|
527
|
-
|
|
528
485
|
/**
|
|
529
486
|
* Повертає відсортовані імена *.mdc у .cursor/rules поточного проєкту
|
|
530
487
|
* @returns {Promise<string[]>} базові імена файлів (лише .mdc)
|
|
@@ -712,10 +669,10 @@ async function syncClaudeMd(ignore) {
|
|
|
712
669
|
lines.push(...buildClaudeLintParallelismSectionLines())
|
|
713
670
|
|
|
714
671
|
const skillsSectionLines = await buildClaudeSkillsSectionLines()
|
|
715
|
-
lines.push(...skillsSectionLines
|
|
672
|
+
lines.push(...skillsSectionLines)
|
|
716
673
|
const claudeMdPath = join(cwd(), 'CLAUDE.md')
|
|
717
674
|
const hadFile = existsSync(claudeMdPath)
|
|
718
|
-
await writeFile(claudeMdPath, lines
|
|
675
|
+
await writeFile(claudeMdPath, formatGeneratedMarkdownLines(lines), 'utf8')
|
|
719
676
|
console.log(hadFile ? `📝 Оновлено CLAUDE.md` : `📝 Створено CLAUDE.md`)
|
|
720
677
|
}
|
|
721
678
|
|
|
@@ -739,8 +696,7 @@ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
|
|
|
739
696
|
const body = renderAgentsTemplate(templateText, mdcFiles, skillItems, commandItems)
|
|
740
697
|
const agentsPath = join(cwd(), AGENTS_FILE)
|
|
741
698
|
const hadFile = existsSync(agentsPath)
|
|
742
|
-
|
|
743
|
-
await writeFile(agentsPath, out, 'utf8')
|
|
699
|
+
await writeFile(agentsPath, body.endsWith('\n') ? body : `${body}\n`, 'utf8')
|
|
744
700
|
console.log(
|
|
745
701
|
hadFile
|
|
746
702
|
? `📝 Оновлено ${AGENTS_FILE} з ${AGENTS_TEMPLATE_FILE}`
|
package/package.json
CHANGED
|
@@ -53,8 +53,8 @@ async function loadNCursorRules() {
|
|
|
53
53
|
*/
|
|
54
54
|
function lintChainHasScript(lintScript, target) {
|
|
55
55
|
if (!lintScript) return false
|
|
56
|
-
const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu,
|
|
57
|
-
return new RegExp(`(
|
|
56
|
+
const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)
|
|
57
|
+
return new RegExp(String.raw`(?:^|\s)bun\s+run\s+${escaped}(?:$|\s)`, 'u').test(lintScript)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -74,6 +74,17 @@ const RULE_SCRIPTS = [
|
|
|
74
74
|
{ rules: ['image-avif', 'image-compress'], script: 'lint-image', doc: 'image-avif.mdc / image-compress.mdc' }
|
|
75
75
|
]
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Загортає кожен ідентифікатор у backticks та зʼєднує через роздільник. Винесено
|
|
79
|
+
* окремою функцією, щоб не нестити template literals у `pass`/`fail`-повідомленнях.
|
|
80
|
+
* @param {string[]} items ідентифікатори правил
|
|
81
|
+
* @param {string} sep роздільник (наприклад `, ` або `/`)
|
|
82
|
+
* @returns {string} рядок виду "`a`, `b`"
|
|
83
|
+
*/
|
|
84
|
+
function backtickJoin(items, sep) {
|
|
85
|
+
return items.map(r => '`' + r + '`').join(sep)
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
/**
|
|
78
89
|
* Описує стан правил-власників скрипта для повідомлень про reason. Повертає або список увімкнених
|
|
79
90
|
* правил (для passing-кейсу «правило є»), або компактний опис, чому всі вимкнені (для inverse-fail).
|
|
@@ -84,7 +95,7 @@ const RULE_SCRIPTS = [
|
|
|
84
95
|
function ownerStatus(owners, cursorRules) {
|
|
85
96
|
const enabled = owners.filter(r => cursorRules.rules.has(r))
|
|
86
97
|
if (enabled.length > 0) {
|
|
87
|
-
return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${enabled
|
|
98
|
+
return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${backtickJoin(enabled, ', ')}` }
|
|
88
99
|
}
|
|
89
100
|
if (owners.length === 1) {
|
|
90
101
|
const [only] = owners
|
|
@@ -93,7 +104,7 @@ function ownerStatus(owners, cursorRules) {
|
|
|
93
104
|
}
|
|
94
105
|
const disabledCount = owners.filter(r => cursorRules.disabled.has(r)).length
|
|
95
106
|
const note = disabledCount === owners.length ? 'усі власники в disable-rules' : 'жоден власник не активний у rules'
|
|
96
|
-
return { enabled, reason: `${owners
|
|
107
|
+
return { enabled, reason: `${backtickJoin(owners, '/')} — ${note}` }
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
/**
|
|
@@ -104,7 +115,7 @@ function ownerStatus(owners, cursorRules) {
|
|
|
104
115
|
* вимкненому правилі: `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево
|
|
105
116
|
* незалежно від конфігу (як було в cursor-репо: `disable-rules: ["k8s"]` + залишений `lint-k8s`
|
|
106
117
|
* ламав chain на template-сорцях власного правила).
|
|
107
|
-
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter
|
|
118
|
+
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter callback-и `pass`/`fail` для звіту
|
|
108
119
|
* @param {Record<string, string>} scripts scripts з package.json
|
|
109
120
|
* @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
|
|
110
121
|
*/
|
|
@@ -127,7 +138,7 @@ function checkCursorRuleScripts(reporter, scripts, cursorRules) {
|
|
|
127
138
|
}
|
|
128
139
|
if (present) {
|
|
129
140
|
fail(
|
|
130
|
-
`У .n-cursor.json немає активних власників ${owners
|
|
141
|
+
`У .n-cursor.json немає активних власників ${backtickJoin(owners, '/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
|
|
131
142
|
)
|
|
132
143
|
}
|
|
133
144
|
if (inChain) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Ефемерне резюме «тертя» з пакетом @nitra/cursor наприкінці кожного скілу — агент називає, що в правилах / SKILL.md / check заважало, без запису у файли
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Правило про **зворотний звʼязок до пакета `@nitra/cursor`**. Виконуючи скіл пакета (`n-lint`, `n-fix`, `n-taze`, `n-adr-normalize`, `n-llm-patch`, `n-publish-telegram`, `mdc-check`), агент проходить крізь правила `.cursor/rules/`, інструкції `SKILL.md` і програмні перевірки `npx @nitra/cursor check`. Саме тут видно, що в пакеті недопрацьовано: неоднозначна інструкція, відсутня перевірка, хибне спрацювання, порушення без автофіксу. Ця інформація цінна для розробників пакета, але без явного кроку зникає разом із сесією.
|
|
8
|
+
|
|
9
|
+
## Коли застосовувати
|
|
10
|
+
|
|
11
|
+
Лише якщо в цій сесії агент **виконав хоча б один скіл** `@nitra/cursor`. Без виконання skills правило інертне — додавати нічого не треба.
|
|
12
|
+
|
|
13
|
+
## Що зробити
|
|
14
|
+
|
|
15
|
+
Наприкінці скілу, **після** основного резюме роботи, додай у відповідь чату секцію `## 🔧 Покращення @nitra/cursor` — але лише якщо під час виконання було помічене **тертя** з пакетом.
|
|
16
|
+
|
|
17
|
+
**Тертя** — усе, що ускладнило роботу скілу й стосується самого пакета `@nitra/cursor`, а не коду користувацького проєкту:
|
|
18
|
+
|
|
19
|
+
- неоднозначна чи неповна інструкція в `SKILL.md` або `.mdc`;
|
|
20
|
+
- правило вимагає поведінку, яку можна перевірити програмно, але `check-*.mjs` для неї немає;
|
|
21
|
+
- хибне спрацювання перевірки (false positive);
|
|
22
|
+
- порушення, яке правило вимагає виправляти вручну, хоча реальний автофікс можливий;
|
|
23
|
+
- повторюваний патерн, який варто закодувати в правило чи скіл.
|
|
24
|
+
|
|
25
|
+
Звичайні порушення **коду користувача**, які скіл і має виправляти, — це **не** тертя.
|
|
26
|
+
|
|
27
|
+
## Формат секції
|
|
28
|
+
|
|
29
|
+
Кожен пункт — за схемою:
|
|
30
|
+
|
|
31
|
+
- **target** — `rule` | `skill` | `check`
|
|
32
|
+
- **id** — який саме (`lint`, `text`, `js-lint`, …)
|
|
33
|
+
- **kind** — `ambiguous-doc` | `missing-check` | `false-positive` | `no-autofix` | `recurring-pattern`
|
|
34
|
+
- **evidence** — конкретний `файл:рядок` або вивід команди з цього запуску
|
|
35
|
+
- **suggestion** — запропонована зміна
|
|
36
|
+
|
|
37
|
+
Приклад:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
## 🔧 Покращення @nitra/cursor
|
|
41
|
+
|
|
42
|
+
- skill `n-lint`, `ambiguous-doc` — крок 2 не каже, як діяти при частковому autofix
|
|
43
|
+
evidence: `oxlint --fix` лишив 3 no-autofix-помилки
|
|
44
|
+
suggestion: додати в SKILL.md підпункт про частковий autofix
|
|
45
|
+
- rule `js-lint`, `missing-check` — немає програмної перевірки jscpd-порогу
|
|
46
|
+
evidence: jscpd впав, але `check js-lint` цього не ловить
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Ефемерність — обовʼязково
|
|
50
|
+
|
|
51
|
+
Резюме живе **тільки в цій відповіді чату**. **Заборонено**:
|
|
52
|
+
|
|
53
|
+
- записувати або створювати файли (чернетки, тимчасові нотатки на диску, будь-що в `docs/`);
|
|
54
|
+
- створювати GitHub issue чи PR;
|
|
55
|
+
- редагувати сам пакет `@nitra/cursor`.
|
|
56
|
+
|
|
57
|
+
Це навмисний вибір: канал має бути легким і миттєвим. Розробник, читаючи відповідь, сам вирішує, чи переносити пункт у пакет.
|
|
58
|
+
|
|
59
|
+
## Чесність
|
|
60
|
+
|
|
61
|
+
- Лише **реальне** тертя, помічене в **цьому** запуску — без припущень і вигаданих прикладів.
|
|
62
|
+
- Якщо тертя не було — **повністю пропусти** секцію; не пиши порожню секцію чи «все добре».
|
|
63
|
+
- Не дублюй у секції переказ роботи скілу — лише конкретні, дієві пропозиції до пакета.
|
|
@@ -53,6 +53,12 @@ import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-
|
|
|
53
53
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
54
54
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
55
55
|
|
|
56
|
+
// Дешеві pre-filter regex'и для AST-сканера LISTEN/NOTIFY: уникаємо парсингу
|
|
57
|
+
// файлів, у яких ніяких сигналів немає. Винесено в модульний скоуп, щоб не
|
|
58
|
+
// перекомпілювати RegExp на кожному виклику `collectPgUsageForFile`.
|
|
59
|
+
const LISTEN_NOTIFY_KEYWORD_RE = /\b(LISTEN|UNLISTEN|NOTIFY)\b/iu
|
|
60
|
+
const NOTIFICATION_LITERAL_RE = /['"`]notification['"`]/u
|
|
61
|
+
|
|
56
62
|
/**
|
|
57
63
|
* Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
|
|
58
64
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
@@ -138,7 +144,7 @@ function collectPgUsageForFile(content, rel, pgUsage) {
|
|
|
138
144
|
// Дешевий pre-filter за текстом: AST-парсинг тільки коли файл містить
|
|
139
145
|
// або імпорт `'pg'`, або хоча б одне зі слів LISTEN / NOTIFY / UNLISTEN /
|
|
140
146
|
// 'notification' — інакше LISTEN/NOTIFY у ньому точно немає.
|
|
141
|
-
const mayHaveListenNotify =
|
|
147
|
+
const mayHaveListenNotify = LISTEN_NOTIFY_KEYWORD_RE.test(content) || NOTIFICATION_LITERAL_RE.test(content)
|
|
142
148
|
if (!textHasPgLibImport(content) && !mayHaveListenNotify) return
|
|
143
149
|
const imports = findPgLibImportInText(content, rel)
|
|
144
150
|
const listenNotify = findPgListenNotifyUsageInText(content, rel)
|
|
@@ -263,7 +269,7 @@ async function checkPgDependencyAndUsage(pkgJsonPaths, repoRoot, pgUsage, report
|
|
|
263
269
|
}
|
|
264
270
|
if (!pkg || typeof pkg !== 'object') continue
|
|
265
271
|
const deps = pkg.dependencies
|
|
266
|
-
if (!deps || typeof deps !== 'object' || !Object.
|
|
272
|
+
if (!deps || typeof deps !== 'object' || !Object.hasOwn(deps, 'pg')) continue
|
|
267
273
|
pgDepsFound++
|
|
268
274
|
if (!hasAnyListenNotify) {
|
|
269
275
|
pgDepFails++
|
|
@@ -397,7 +403,7 @@ export async function check() {
|
|
|
397
403
|
}
|
|
398
404
|
if (unsafeTemplateInterp === 0) {
|
|
399
405
|
pass(
|
|
400
|
-
'js-bun-db: немає sql.unsafe(
|
|
406
|
+
'js-bun-db: немає sql.unsafe(template literal з інтерполяцією) ' +
|
|
401
407
|
'(identifiers через @scaleleap/pg-format %I, values — позиційні $N)'
|
|
402
408
|
)
|
|
403
409
|
}
|
|
@@ -355,7 +355,7 @@ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_ad
|
|
|
355
355
|
|
|
356
356
|
### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
|
|
357
357
|
|
|
358
|
-
`sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна
|
|
358
|
+
`sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка`${x}` у `sql.unsafe`-рядок:
|
|
359
359
|
|
|
360
360
|
- **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
|
|
361
361
|
- **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
|
|
@@ -379,7 +379,7 @@ const query = format('CREATE TABLE %I (id int)', tableName)
|
|
|
379
379
|
await sql.unsafe(query)
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
-
Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`)
|
|
382
|
+
Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і`sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності`// allow-unsafe`-маркера).
|
|
383
383
|
|
|
384
384
|
❌ Заборонені кейси (треба переробити на tagged template):
|
|
385
385
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FS-частина правила `security`: concern `sample_secret`.
|
|
3
|
+
*
|
|
4
|
+
* Перевіряє, що фейкові credential-значення у *прикладних* файлах записані як
|
|
5
|
+
* канонічний placeholder `sample-secret`, а не як bare `secret`.
|
|
6
|
+
*
|
|
7
|
+
* `sample-secret` містить підрядок `sample`, який є у вшитому списку
|
|
8
|
+
* `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює
|
|
9
|
+
* гарантовано й незалежно від версії. Bare `secret` наразі не фіксується сканером
|
|
10
|
+
* лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка поведінка,
|
|
11
|
+
* що залежить від версії інструмента, на яку не варто покладатися.
|
|
12
|
+
*
|
|
13
|
+
* Прикладними вважаються файли, чий basename має суфікс `.example` / `.sample`
|
|
14
|
+
* / `.template` / `.dist` або infix `.example.` / `.sample.` / `.template.`, а
|
|
15
|
+
* також будь-які файли всередині каталогів `fixtures` / `fixture` /
|
|
16
|
+
* `__fixtures__`. Решта файлів не сканується — там `secret` майже завжди
|
|
17
|
+
* частина реального коду, а не placeholder.
|
|
18
|
+
*
|
|
19
|
+
* Порушенням є лише `secret` у *позиції значення* — одразу після `=`, `:` чи
|
|
20
|
+
* `=>` (з опційними лапками). Імена ключів (`client_secret`, `JWT_SECRET`) не
|
|
21
|
+
* чіпаються: матч прив'язаний до значення, не до ключа.
|
|
22
|
+
*
|
|
23
|
+
* Чому regex, а не AST: прикладні файли — різнорідні конфіги (`.env`, YAML,
|
|
24
|
+
* JSON, TOML, plain `.dist`), єдиного AST для них немає, тож скан порядковий.
|
|
25
|
+
* Чому JS, а не Rego: щоб знайти прикладні файли, треба обійти дерево
|
|
26
|
+
* (`readdir`), а вміст — неструктурований текст (conftest парсить лише
|
|
27
|
+
* структуровані документи).
|
|
28
|
+
*/
|
|
29
|
+
import { readFile } from 'node:fs/promises'
|
|
30
|
+
import { relative, sep } from 'node:path'
|
|
31
|
+
|
|
32
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
33
|
+
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
34
|
+
|
|
35
|
+
/** Суфікс basename'а прикладного файлу (`config.example`, `.env.dist`). */
|
|
36
|
+
const EXAMPLE_SUFFIX_RE = /\.(?:example|sample|template|dist)$/iu
|
|
37
|
+
|
|
38
|
+
/** Infix у basename'і (`docker-compose.example.yml`, `app.config.sample.json`). */
|
|
39
|
+
const EXAMPLE_INFIX_RE = /\.(?:example|sample|template)\./iu
|
|
40
|
+
|
|
41
|
+
/** Сегмент шляху з фікстурами (`fixtures/`, `fixture/`, `__fixtures__/`). */
|
|
42
|
+
const FIXTURE_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?)(?:\/|$)/u
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bare-`secret` у позиції значення: після `=`, `:` або `=>` (опційні лапки), а
|
|
46
|
+
* далі лише пробіли / завершальна пунктуація / коментар до кінця рядка. Прив'язка
|
|
47
|
+
* до `$` гарантує, що `secret` — увесь токен значення (`secret-key`, `secretValue`
|
|
48
|
+
* не матчаться); прив'язка до `[:=]` відсікає імена ключів (`client_secret`).
|
|
49
|
+
* Без урахування регістру символів.
|
|
50
|
+
*/
|
|
51
|
+
const VALUE_SECRET_RE = /[:=]>?\s*(['"]?)secret\1[\s,;}\])]*(?:(?:#|\/\/).*)?$/iu
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Чи є файл «прикладним» — таким, де `secret` очікувано є placeholder'ом.
|
|
55
|
+
* @param {string} relPosix відносний шлях від cwd у posix-форматі
|
|
56
|
+
* @returns {boolean} `true`, якщо файл треба сканувати
|
|
57
|
+
*/
|
|
58
|
+
function isExampleFile(relPosix) {
|
|
59
|
+
const base = relPosix.slice(relPosix.lastIndexOf('/') + 1)
|
|
60
|
+
return EXAMPLE_SUFFIX_RE.test(base) || EXAMPLE_INFIX_RE.test(base) || FIXTURE_DIR_RE.test(relPosix)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {Promise<number>} exit-код перевірки (0 — OK, 1 — є bare `secret`)
|
|
65
|
+
*/
|
|
66
|
+
export async function check() {
|
|
67
|
+
const reporter = createCheckReporter()
|
|
68
|
+
const { pass, fail } = reporter
|
|
69
|
+
const cwd = process.cwd()
|
|
70
|
+
|
|
71
|
+
/** @type {Array<{ abs: string, rel: string }>} */
|
|
72
|
+
const examples = []
|
|
73
|
+
await walkDir(cwd, abs => {
|
|
74
|
+
const rel = relative(cwd, abs).split(sep).join('/')
|
|
75
|
+
if (isExampleFile(rel)) examples.push({ abs, rel })
|
|
76
|
+
})
|
|
77
|
+
examples.sort((a, b) => a.rel.localeCompare(b.rel))
|
|
78
|
+
|
|
79
|
+
if (examples.length === 0) {
|
|
80
|
+
pass('прикладних файлів не знайдено — placeholder перевіряти нема де')
|
|
81
|
+
return reporter.getExitCode()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let violations = 0
|
|
85
|
+
for (const { abs, rel } of examples) {
|
|
86
|
+
let content
|
|
87
|
+
try {
|
|
88
|
+
content = await readFile(abs, 'utf8')
|
|
89
|
+
} catch {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const lines = content.split('\n')
|
|
93
|
+
for (const [i, line_] of lines.entries()) {
|
|
94
|
+
const line = line_.endsWith('\r') ? line_.slice(0, -1) : line_
|
|
95
|
+
if (!VALUE_SECRET_RE.test(line)) continue
|
|
96
|
+
violations++
|
|
97
|
+
fail(`${rel}:${i + 1}: \`${line.trim()}\` — заміни placeholder \`secret\` на \`sample-secret\` (security.mdc)`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (violations === 0) {
|
|
102
|
+
pass(`прикладні файли (${examples.length}) не містять bare \`secret\``)
|
|
103
|
+
}
|
|
104
|
+
return reporter.getExitCode()
|
|
105
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Локальний та CI-секюріті-лінт через TruffleHog — скрипт `lint-security`, `.trufflehog-exclude`, інтеграція в агрегований `lint`
|
|
2
|
+
description: Локальний та CI-секюріті-лінт через TruffleHog — скрипт `lint-security`, `.trufflehog-exclude`, інтеграція в агрегований `lint`; канонічний placeholder `sample-secret` у прикладних файлах
|
|
3
3
|
globs: "**/.trufflehog-exclude,**/package.json,**/.github/workflows/**/*.yml"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '2.
|
|
5
|
+
version: '2.1'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
[TruffleHog](https://github.com/trufflesecurity/trufflehog) — глобальний CLI (як `shellcheck`, `conftest`); **не** додавай до `dependencies`/`devDependencies`.
|
|
@@ -35,3 +35,18 @@ Workflow обовʼязковий — забезпечує незалежний
|
|
|
35
35
|
- Канон: [lint-security.yml.snippet.yml](./policy/lint_security_yml/template/lint-security.yml.snippet.yml)
|
|
36
36
|
|
|
37
37
|
Перевіряється policy `security.lint_security_yml`: серед `uses:` має бути крок з `trufflesecurity/trufflehog@main`. Універсальні workflow-перевірки (checkout, permissions, persist-credentials) — у `ga.workflow_common`. Для повного скану історії потрібен `fetch-depth: 0`.
|
|
38
|
+
|
|
39
|
+
## Placeholder для секретів — `sample-secret`
|
|
40
|
+
|
|
41
|
+
Фейкові credential-значення у **прикладних файлах** (`.env.example`, `.env.dist`, `*.example`, `*.sample`, `*.template`, вміст каталогів `fixtures/`) пиши як `sample-secret`, а не як bare `secret`.
|
|
42
|
+
|
|
43
|
+
`sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює **гарантовано** й незалежно від версії. Bare `secret` наразі не фіксується сканером лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка поведінка, що залежить від версії інструмента.
|
|
44
|
+
|
|
45
|
+
- Правильно: `DB_PASSWORD=sample-secret`, `password: "sample-secret"`
|
|
46
|
+
- Неправильно: `DB_PASSWORD=secret`, `password: "secret"`
|
|
47
|
+
|
|
48
|
+
Перевіряється лише `secret` у позиції значення (після `=`, `:`, `=>`); імена ключів на кшталт `client_secret` не чіпаються. Concern `security.sample_secret` — деталі скану в `fix/sample_secret/check.mjs`.
|
|
49
|
+
|
|
50
|
+
## Перевірка
|
|
51
|
+
|
|
52
|
+
`npx @nitra/cursor check security`
|
|
@@ -784,9 +784,10 @@ export function findPgLibImportInText(content, virtualPath = 'scan.ts') {
|
|
|
784
784
|
* `LISTEN ` / `UNLISTEN ` / `NOTIFY ` (case-insensitive);
|
|
785
785
|
* - `<obj>.on('notification', ...)` — pg-listener notification-подій (другий
|
|
786
786
|
* аргумент — функція; перший — точно рядок `'notification'`);
|
|
787
|
-
* - TaggedTemplateExpression виду
|
|
788
|
-
*
|
|
789
|
-
*
|
|
787
|
+
* - TaggedTemplateExpression виду sql tagged template з LISTEN/UNLISTEN/NOTIFY
|
|
788
|
+
* на початку першого quasi — на випадок, якщо хтось використовує Bun
|
|
789
|
+
* SQL-tagged-template, а LISTEN/NOTIFY все одно лишається у тексті запиту
|
|
790
|
+
* (це не запрацює у Bun SQL, але як сигнал — приймаємо).
|
|
790
791
|
*
|
|
791
792
|
* Регістр SQL-слів не важливий, провідні пробіли допускаються.
|
|
792
793
|
* @param {string} content вихідний код
|
|
@@ -873,8 +874,8 @@ function listenNotifyFromCallExpression(node) {
|
|
|
873
874
|
}
|
|
874
875
|
|
|
875
876
|
/**
|
|
876
|
-
* Аналізує TaggedTemplateExpression
|
|
877
|
-
*
|
|
877
|
+
* Аналізує TaggedTemplateExpression: якщо перший quasi починається з
|
|
878
|
+
* LISTEN/UNLISTEN/NOTIFY — повертає відповідний kind.
|
|
878
879
|
* @param {Record<string, unknown>} node AST node
|
|
879
880
|
* @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | null} kind знахідки
|
|
880
881
|
*/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Утиліти генерації AGENTS.md / CLAUDE.md з шаблонів CLI `n-cursor`.
|
|
3
|
+
*
|
|
4
|
+
* Після розгортання Mustache-секцій і збирання рядків CLAUDE.md нормалізує markdown,
|
|
5
|
+
* щоб не лишати подвійні порожні рядки (MD012) між пунктами списку чи на стиках секцій.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Згортає три й більше послідовних `\n` до рівно двох (один порожній рядок між блоками).
|
|
10
|
+
* @param {string} text вихідний markdown
|
|
11
|
+
* @returns {string} markdown без послідовностей з трьох і більше `\n`
|
|
12
|
+
*/
|
|
13
|
+
export function collapseMultipleBlankLines(text) {
|
|
14
|
+
return String(text).replaceAll(/\n{3,}/g, '\n\n')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Розгортає блок Mustache `{{#section}}…{{/section}}` для масиву елементів.
|
|
19
|
+
* Після `trim` тіла секції елементи зʼєднуються одним `\n` без зайвих порожніх рядків між ними.
|
|
20
|
+
* @param {string} template вихідний текст шаблону
|
|
21
|
+
* @param {string} section ім'я секції (наприклад services)
|
|
22
|
+
* @param {Record<string, string>[]} items елементи для повторення тіла секції
|
|
23
|
+
* @param {string} prop ключ поля для підстановки замість `{{prop}}`
|
|
24
|
+
* @returns {string} шаблон після підстановки всіх входжень блоку секції
|
|
25
|
+
*/
|
|
26
|
+
export function expandMustacheSection(template, section, items, prop) {
|
|
27
|
+
const open = `{{#${section}}}`
|
|
28
|
+
const close = `{{/${section}}}`
|
|
29
|
+
const placeholder = `{{${prop}}}`
|
|
30
|
+
let result = template
|
|
31
|
+
let start = result.indexOf(open)
|
|
32
|
+
let end = result.indexOf(close)
|
|
33
|
+
while (start !== -1 && end !== -1 && end > start) {
|
|
34
|
+
const inner = result.slice(start + open.length, end).trim()
|
|
35
|
+
const rendered = items.map(item => inner.split(placeholder).join(String(item[prop]))).join('\n')
|
|
36
|
+
result = result.slice(0, start) + rendered + result.slice(end + close.length)
|
|
37
|
+
start = result.indexOf(open)
|
|
38
|
+
end = result.indexOf(close)
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Підставляє у AGENTS.template.md списки правил, skills і команд.
|
|
45
|
+
* @param {string} templateText вміст AGENTS.template.md
|
|
46
|
+
* @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
|
|
47
|
+
* @param {{ name: string }[]} skillItems рядки для секції Skills
|
|
48
|
+
* @param {{ name: string }[]} commandItems рядки для секції commands
|
|
49
|
+
* @returns {string} готовий вміст AGENTS.md без подвійних порожніх рядків у списках
|
|
50
|
+
*/
|
|
51
|
+
export function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems) {
|
|
52
|
+
let result = templateText
|
|
53
|
+
const serviceItems = mdcBasenames.map(mdcName => ({
|
|
54
|
+
name: `- .cursor/rules/${mdcName}`
|
|
55
|
+
}))
|
|
56
|
+
result = expandMustacheSection(result, 'services', serviceItems, 'name')
|
|
57
|
+
result = expandMustacheSection(result, 'skills', skillItems, 'name')
|
|
58
|
+
result = expandMustacheSection(result, 'commands', commandItems, 'name')
|
|
59
|
+
return collapseMultipleBlankLines(result)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Збирає markdown з рядків, прибираючи подвійні порожні рядки на стиках секцій.
|
|
64
|
+
* @param {string[]} lines рядки документа
|
|
65
|
+
* @returns {string} зібраний markdown із завершальним `\n`
|
|
66
|
+
*/
|
|
67
|
+
export function formatGeneratedMarkdownLines(lines) {
|
|
68
|
+
const text = lines.join('\n')
|
|
69
|
+
const collapsed = collapseMultipleBlankLines(text)
|
|
70
|
+
return collapsed.endsWith('\n') ? collapsed : `${collapsed}\n`
|
|
71
|
+
}
|