@nitra/cursor 1.8.16 → 1.8.18

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/mdc/js-lint.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Перевірка JavaScript коду
3
3
  alwaysApply: true
4
- version: '1.8'
4
+ version: '1.9'
5
5
  ---
6
6
 
7
7
  **oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`:** `oxlint` (без `bunx`), **`bunx eslint`**, **`bunx jscpd`**; у CI — `bunx oxlint` / `bunx eslint` / `bunx jscpd`. Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
@@ -38,7 +38,7 @@ version: '1.8'
38
38
  "exitCode": 1,
39
39
  "reporters": ["console"],
40
40
  "minLines": 25,
41
- "ignore": []
41
+ "ignore": ["**/dist/**",]
42
42
  }
43
43
  ```
44
44
 
@@ -132,9 +132,7 @@ export default [
132
132
 
133
133
  ## Тести
134
134
 
135
- Проекту повинен бути покритий unit тестами за допомогою Bun test.
136
-
137
- **Код:** синтаксис Node **24+**, **top level await**.
135
+ Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
138
136
 
139
137
  ## Перевірка
140
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.16",
3
+ "version": "1.8.18",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -35,10 +35,10 @@
35
35
  "scripts": {
36
36
  "test": "bun test tests"
37
37
  },
38
- "engines": {
39
- "node": ">=24"
40
- },
41
38
  "dependencies": {
42
39
  "yaml": "^2.8.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=24"
43
43
  }
44
44
  }
@@ -1,14 +1,42 @@
1
1
  /**
2
2
  * Перевіряє лінт JavaScript за правилом js-lint.mdc.
3
3
  *
4
- * Flat ESLint, скрипт `lint-js` (oxlint, eslint, jscpd), `engines.node`, без prettier,
5
- * наявність `.jscpd.json` і workflow `lint-js.yml`.
4
+ * Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
5
+ * `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow `lint-js.yml` (checkout@v6,
6
+ * setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24. Дубль перевірки JS у `lint.yml` —
7
+ * заборонено.
6
8
  */
7
9
  import { existsSync } from 'node:fs'
8
10
  import { readFile } from 'node:fs/promises'
9
11
 
10
12
  import { pass } from './utils/pass.mjs'
11
13
 
14
+ /** Очікуваний локальний скрипт (oxlint без bunx; eslint/jscpd через bunx). */
15
+ export const CANONICAL_LINT_JS = 'oxlint --fix && bunx eslint --fix . && bunx jscpd .'
16
+
17
+ /** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
18
+ export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
19
+
20
+ /**
21
+ * Нормалізує рядок скрипта для порівняння (зайві пробіли).
22
+ * @param {string} s вихідний рядок скрипта `lint-js`
23
+ * @returns {string} рядок без зайвих пробілів на краях і з одиничними пробілами всередині
24
+ */
25
+ export function normalizeLintJsScript(s) {
26
+ return String(s).trim().replaceAll(/\s+/gu, ' ')
27
+ }
28
+
29
+ /**
30
+ * Чи рядок `lint-js` збігається з каноном і без `bunx oxlint`.
31
+ * @param {string} script значення `scripts.lint-js` з package.json
32
+ * @returns {boolean} true, якщо рядок канонічний
33
+ */
34
+ export function isCanonicalLintJs(script) {
35
+ const n = normalizeLintJsScript(script)
36
+ if (n.includes('bunx oxlint')) return false
37
+ return n === CANONICAL_LINT_JS
38
+ }
39
+
12
40
  /**
13
41
  * Перевіряє відповідність проєкту правилам js-lint.mdc
14
42
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -20,12 +48,34 @@ export async function check() {
20
48
  exitCode = 1
21
49
  }
22
50
 
51
+ let eslintPath = ''
23
52
  if (existsSync('eslint.config.js')) {
53
+ eslintPath = 'eslint.config.js'
24
54
  pass('eslint.config.js існує')
25
55
  } else if (existsSync('eslint.config.mjs')) {
56
+ eslintPath = 'eslint.config.mjs'
26
57
  pass('eslint.config.mjs існує')
27
58
  } else {
28
- fail('Відсутній eslint.config.js — створи його з getConfig від @nitra/eslint-config')
59
+ fail('Відсутній eslint.config.js або eslint.config.mjs flat config з getConfig (js-lint.mdc)')
60
+ }
61
+
62
+ if (eslintPath) {
63
+ const eslintRaw = await readFile(eslintPath, 'utf8')
64
+ if (eslintRaw.includes('getConfig')) {
65
+ pass(`${eslintPath}: містить getConfig`)
66
+ } else {
67
+ fail(`${eslintPath}: потрібен виклик getConfig (js-lint.mdc)`)
68
+ }
69
+ if (eslintRaw.includes('@nitra/eslint-config')) {
70
+ pass(`${eslintPath}: імпорт @nitra/eslint-config`)
71
+ } else {
72
+ fail(`${eslintPath}: імпортуй getConfig з @nitra/eslint-config`)
73
+ }
74
+ if (eslintRaw.includes('**/auto-imports.d.ts')) {
75
+ pass(`${eslintPath}: ignores містить **/auto-imports.d.ts`)
76
+ } else {
77
+ fail(`${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js-lint.mdc)`)
78
+ }
29
79
  }
30
80
 
31
81
  if (existsSync('package.json')) {
@@ -34,38 +84,25 @@ export async function check() {
34
84
  if (pkg.scripts?.['lint-js']) {
35
85
  pass('package.json містить скрипт lint-js')
36
86
  const lintJs = String(pkg.scripts['lint-js'])
37
- if (lintJs.includes('jscpd')) {
38
- pass('lint-js містить jscpd')
39
- } else {
40
- fail('lint-js має викликати jscpd — додай "&& bunx jscpd ." у кінець скрипта')
41
- }
42
- if (lintJs.includes('bunx eslint')) {
43
- pass('lint-js викликає bunx eslint')
44
- } else {
45
- fail('lint-js має містити bunx eslint (n-js-lint.mdc)')
46
- }
47
- if (lintJs.includes('bunx jscpd')) {
48
- pass('lint-js викликає bunx jscpd')
49
- } else {
50
- fail('lint-js має містити bunx jscpd (n-js-lint.mdc)')
51
- }
52
- if (lintJs.includes('oxlint')) {
53
- pass('lint-js містить oxlint')
87
+ if (isCanonicalLintJs(lintJs)) {
88
+ pass(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
54
89
  } else {
55
- fail('lint-js має містити oxlint (n-js-lint.mdc)')
90
+ fail(
91
+ `lint-js має бути рівно: "${CANONICAL_LINT_JS}" (oxlint без bunx; див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
92
+ )
56
93
  }
57
94
  } else {
58
- fail('package.json не містить скрипт "lint-js" — додай: "oxlint --fix && bunx eslint --fix . && bunx jscpd ."')
95
+ fail(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
59
96
  }
60
97
 
61
98
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
62
99
  if (allDeps.prettier) {
63
- fail('package.json: видали залежність prettier (oxfmt замість prettier, n-js-lint.mdc)')
100
+ fail('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
64
101
  } else {
65
102
  pass('package.json не містить prettier')
66
103
  }
67
104
  if (allDeps['@nitra/prettier-config']) {
68
- fail('package.json: видали @nitra/prettier-config (n-js-lint.mdc)')
105
+ fail('package.json: видали @nitra/prettier-config (js-lint.mdc)')
69
106
  } else {
70
107
  pass('package.json не містить @nitra/prettier-config')
71
108
  }
@@ -78,7 +115,7 @@ export async function check() {
78
115
 
79
116
  const nodeEngine = pkg.engines?.node
80
117
  if (nodeEngine) {
81
- const match = nodeEngine.match(/(\d+)/)
118
+ const match = nodeEngine.match(/(\d+)/u)
82
119
  if (match && Number(match[1]) >= 24) {
83
120
  pass(`engines.node: "${nodeEngine}"`)
84
121
  } else {
@@ -89,26 +126,68 @@ export async function check() {
89
126
  }
90
127
  }
91
128
 
129
+ if (existsSync('.vscode/extensions.json')) {
130
+ let ext
131
+ try {
132
+ ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
133
+ } catch {
134
+ fail('.vscode/extensions.json не є валідним JSON')
135
+ ext = null
136
+ }
137
+ if (ext) {
138
+ const rec = ext.recommendations
139
+ if (Array.isArray(rec)) {
140
+ const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
141
+ if (missing.length > 0) {
142
+ fail(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
143
+ } else {
144
+ pass('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
145
+ }
146
+ } else {
147
+ fail('.vscode/extensions.json: поле recommendations має бути масивом')
148
+ }
149
+ }
150
+ } else {
151
+ fail('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
152
+ }
153
+
92
154
  if (existsSync('.github/workflows/lint-js.yml')) {
93
155
  const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
94
156
  pass('lint-js.yml існує')
95
- if (content.includes('oxlint')) {
96
- pass('lint-js.yml містить oxlint')
97
- } else {
98
- fail('lint-js.yml не містить oxlint')
157
+ const checks = [
158
+ ['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
159
+ ['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
160
+ ['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
161
+ ['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
162
+ ['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
163
+ ['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
164
+ ]
165
+ for (const [needle, errMsg] of checks) {
166
+ if (content.includes(needle)) {
167
+ pass(`lint-js.yml містить: ${needle}`)
168
+ } else {
169
+ fail(errMsg)
170
+ }
99
171
  }
100
- if (content.includes('eslint')) {
101
- pass('lint-js.yml містить eslint')
102
- } else {
103
- fail('lint-js.yml не містить eslint')
172
+ if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
173
+ fail('lint-js.yml: у CI не використовуй oxlint --fix (лише bunx oxlint)')
104
174
  }
105
- if (content.includes('jscpd')) {
106
- pass('lint-js.yml містить jscpd')
107
- } else {
108
- fail('lint-js.yml не містить jscpd — додай крок bunx jscpd .')
175
+ if (content.includes('eslint --fix')) {
176
+ fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
109
177
  }
110
178
  } else {
111
- fail('.github/workflows/lint-js.yml не існує — створи його')
179
+ fail('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
180
+ }
181
+
182
+ if (existsSync('.github/workflows/lint.yml')) {
183
+ const lintYml = await readFile('.github/workflows/lint.yml', 'utf8')
184
+ const looksLikeJsLint =
185
+ /\bbunx\s+oxlint\b/u.test(lintYml) && /\bbunx\s+eslint\b/u.test(lintYml) && /\bjscpd\b/u.test(lintYml)
186
+ if (looksLikeJsLint) {
187
+ fail('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js-lint.mdc)')
188
+ } else {
189
+ pass('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
190
+ }
112
191
  }
113
192
 
114
193
  if (existsSync('.jscpd.json')) {
@@ -131,13 +210,25 @@ export async function check() {
131
210
  } else {
132
211
  fail('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
133
212
  }
213
+ const reporters = jscpdCfg.reporters
214
+ if (Array.isArray(reporters) && reporters.includes('console')) {
215
+ pass('.jscpd.json: reporters містить console')
216
+ } else {
217
+ fail('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
218
+ }
219
+ const minLines = jscpdCfg.minLines
220
+ if (typeof minLines === 'number' && minLines >= 25) {
221
+ pass(`.jscpd.json: minLines ${minLines} (>=25)`)
222
+ } else {
223
+ fail('.jscpd.json має містити "minLines" як число >= 25')
224
+ }
134
225
  }
135
226
  } else {
136
- fail('.jscpd.json не існує — створи з gitignore, exitCode та reporters згідно js-lint.mdc')
227
+ fail('.jscpd.json не існує — створи з полями згідно check js-lint')
137
228
  }
138
229
 
139
230
  for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
140
- if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй eslint.config.js`)
231
+ if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
141
232
  }
142
233
 
143
234
  return exitCode
@@ -207,17 +207,19 @@ function extractApiVersionAndKind(doc) {
207
207
 
208
208
  /**
209
209
  * Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
210
- * @param {unknown} manifest корінь розпарсеного YAML-документа
210
+ * @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
211
211
  * @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
212
212
  */
213
213
  export function deploymentResourcesViolation(manifest) {
214
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest)) return null
214
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
215
+ return null
215
216
  const rec = /** @type {Record<string, unknown>} */ (manifest)
216
217
  if (rec.kind !== 'Deployment') return null
217
218
  const spec = rec.spec
218
219
  if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
219
220
  const template = /** @type {Record<string, unknown>} */ (spec).template
220
- if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template)) return null
221
+ if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
222
+ return null
221
223
  const podSpec = /** @type {Record<string, unknown>} */ (template).spec
222
224
  if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
223
225
  const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
@@ -143,10 +143,11 @@ export function nginxTemplateViolations(content) {
143
143
  if (!ok(content)) return msg
144
144
  }
145
145
 
146
+ // cspell:ignore fastcgi uwsgi
146
147
  const proxyLike =
147
148
  /\b(proxy_pass|proxy_redirect|proxy_set_header|proxy_http_version|fastcgi_pass|grpc_pass|uwsgi_pass)\b/u
148
149
  if (proxyLike.test(content)) {
149
- return 'знайдено proxy/fastcgi/grpc — прибери з шаблону, логіку винеси в HTTPRoute (k8s) (див. nginx-default-tpl.mdc)'
150
+ return 'знайдено proxy, gRPC або інший *_pass до бекенду — прибери з шаблону, логіку винеси в HTTPRoute (k8s) (див. nginx-default-tpl.mdc)'
150
151
  }
151
152
 
152
153
  return null
@@ -158,7 +159,8 @@ export function nginxTemplateViolations(content) {
158
159
  * @returns {boolean} true, якщо структура збігається з прикладом у nginx-default-tpl.mdc
159
160
  */
160
161
  export function httpRouteMatchesNginxDefaultTpl(manifest) {
161
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest)) return false
162
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
163
+ return false
162
164
  const m = /** @type {Record<string, unknown>} */ (manifest)
163
165
  if (m.kind !== 'HTTPRoute') return false
164
166
  const spec = m.spec
@@ -226,7 +228,7 @@ export function httpRouteMatchesNginxDefaultTpl(manifest) {
226
228
  export function iniKeysMissingInTemplate(keys, template) {
227
229
  for (const k of keys) {
228
230
  if (!template.includes(`$${k}`)) {
229
- return `змінна "${k}" з *.ini не використовується в шаблоні — вилучи її з ini або додай плейсхолдер $${k} (див. nginx-default-tpl.mdc)`
231
+ return `змінна "${k}" з *.ini не використовується в шаблоні — вилучи її з ini або додай у шаблон $${k} (див. nginx-default-tpl.mdc)`
230
232
  }
231
233
  }
232
234
  return null