@nitra/cursor 1.5.3 → 1.5.5

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.4'
4
+ version: '1.5'
5
5
  ---
6
6
 
7
7
  Перевірка виконується за допомогою **oxlint**, **ESLint** та **jscpd** (дублікати коду).
@@ -43,6 +43,17 @@ version: '1.4'
43
43
  }
44
44
  ```
45
45
 
46
+ ## jscpd: рефакторинг і структура
47
+
48
+ Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
49
+
50
+ - **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
51
+ - **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
52
+
53
+ Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
54
+
55
+ Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
56
+
46
57
  Додай workflow `.github/workflows/lint-js.yml`:
47
58
 
48
59
  ```yaml
package/mdc/js-pino.mdc CHANGED
@@ -7,7 +7,7 @@ version: '1.0'
7
7
  Проект використовує @nitra/pino для логування.
8
8
  Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino.
9
9
 
10
- В k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
10
+ В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
11
11
  а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
12
12
 
13
13
  Configmap повинен бути з тією самою назвою, що і deployment, якщо у deployment тільки один configmap.
@@ -13,6 +13,52 @@ version: '1.0'
13
13
  - npm/ - файли для npm модуля
14
14
  - package.json
15
15
 
16
+ ## npm publish (GitHub Actions)
17
+
18
+ У `.github/workflows/` має бути workflow `npm-publish.yml`, який публікує пакет з `npm/package.json` на npm при push у `main`, коли змінюється дерево `npm/**`.
19
+
20
+ Приклад вмісту:
21
+
22
+ ```yaml title=".github/workflows/npm-publish.yml"
23
+ name: npm-publish
24
+
25
+ on:
26
+ push:
27
+ paths:
28
+ - 'npm/**'
29
+ branches:
30
+ - main
31
+
32
+ concurrency:
33
+ group: ${{ github.ref }}-${{ github.workflow }}
34
+ cancel-in-progress: true
35
+
36
+ jobs:
37
+ publish:
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ contents: read
41
+ id-token: write # КРИТИЧНО для OIDC!
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ with:
46
+ persist-credentials: false
47
+
48
+ - uses: actions/setup-node@v5
49
+ with:
50
+ node-version: '24' # includes npm@11.6.0
51
+ registry-url: 'https://registry.npmjs.org'
52
+
53
+ - name: Publish package
54
+ uses: JS-DevTools/npm-publish@v4
55
+ with:
56
+ package: npm/package.json
57
+ ```
58
+
59
+ - `id-token: write` потрібен для **trusted publishing** (OIDC) на npm, якщо пакет налаштований у npm без класичного токена в секретах.
60
+ - Шлях `package: npm/package.json` узгоджений зі структурою monorepo (`npm/` — каталог модуля).
61
+
16
62
  ## Перевірка
17
63
 
18
64
  `npx @nitra/cursor check npm-module`
package/mdc/text.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Обробка та перевірка текстових файлів (cspell, markdownlint-cli2, v8r, CI)
3
3
  alwaysApply: true
4
- version: '1.12'
4
+ version: '1.13'
5
5
  ---
6
6
 
7
7
  Правило описує роботу з **текстовими файлами** в репозиторії: перевірка правопису через **cspell**, стиль **Markdown** через **markdownlint-cli2**, валідація **JSON/YAML/TOML** через **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** у Cursor/VS Code та інтеграція в CI.
@@ -210,9 +210,15 @@ jobs:
210
210
 
211
211
  Підлаштуй `language` під проєкт (наприклад додай `ru-ru`, якщо потрібна перевірка російською). Порядок у `import` може впливати на пріоритет словників — тримай корпоративний `@nitra/cspell-dict` там, де зручно для ваших правил.
212
212
 
213
- ## Локальні виключення
213
+ ## Локальні виключення (cspell `words`)
214
214
 
215
- У секції `words` у `.cspell.json` додають власні терміни, імена та скорочення, яких немає в словниках.
215
+ Коли **cspell** підсвічує слово, спочатку **виправ текст**, а не розширюй словник:
216
+
217
+ - виправ **друкарські помилки** та неправильні форми;
218
+ - **перефразуй коректною українською** (або англійською, залежно від контексту файлу): заміни кальки й випадкові склади на звичні формулювання зі словників;
219
+ - заміни **жаргон**, якщо є природний еквівалент у тому ж стилі документації (наприклад, у коментарях пиши «функція зворотного виклику» замість розмовного запозичення з англійського `callback`).
220
+
221
+ У секцію `words` у `.cspell.json` додавай записи **лише якщо переписати коректно неможливо** або **недоречно**: власні назви, стабільні технічні терміни без усталеного перекладу в проєкті, ідентифікатори зовнішніх API тощо.
216
222
 
217
223
  ## Інші мови
218
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,10 +1,51 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
 
4
5
  import { pass } from './utils/pass.mjs'
6
+ import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
5
7
 
6
8
  /**
7
- * Перевіряє відповідність проєкту правилам js-pino.mdc
9
+ * Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
10
+ * @param {string} rootDir відносний шлях workspace (не `'.'`)
11
+ * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
12
+ * @returns {Promise<void>} завершується після перевірок цього пакета
13
+ */
14
+ async function checkWorkspacePackage(rootDir, fail) {
15
+ const label = `[${rootDir}] `
16
+ const pkgPath = join(rootDir, 'package.json')
17
+ if (existsSync(pkgPath)) {
18
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
19
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
20
+
21
+ if (allDeps['@nitra/bunyan']) {
22
+ fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
23
+ }
24
+ if (allDeps.bunyan) {
25
+ fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
26
+ }
27
+ }
28
+
29
+ const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
30
+ if (existsSync(configmapPath)) {
31
+ const content = await readFile(configmapPath, 'utf8')
32
+ if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
33
+ pass(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
34
+ if (content.includes('service.name=') && content.includes('service.namespace=')) {
35
+ pass(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
36
+ } else {
37
+ fail(
38
+ `${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`
39
+ )
40
+ }
41
+ } else {
42
+ fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Перевіряє відповідність проєкту правилам js-pino.mdc лише для workspace-пакетів (не корінь репо).
8
49
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
9
50
  */
10
51
  export async function check() {
@@ -14,26 +55,18 @@ export async function check() {
14
55
  exitCode = 1
15
56
  }
16
57
 
17
- if (existsSync('package.json')) {
18
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
19
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
58
+ const roots = await getMonorepoPackageRootDirs()
59
+ const workspaceRoots = roots.filter(r => r !== '.')
20
60
 
21
- if (allDeps['@nitra/bunyan']) fail('@nitra/bunyan знайдено — замінити на @nitra/pino')
22
- if (allDeps.bunyan) fail('bunyan знайдено — замінити на @nitra/pino')
61
+ if (workspaceRoots.length === 0) {
62
+ pass(
63
+ 'js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено'
64
+ )
65
+ return exitCode
23
66
  }
24
67
 
25
- if (existsSync('k8s/base/configmap.yaml')) {
26
- const content = await readFile('k8s/base/configmap.yaml', 'utf8')
27
- if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
28
- pass('k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES')
29
- if (content.includes('service.name=') && content.includes('service.namespace=')) {
30
- pass('OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace')
31
- } else {
32
- fail('OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>')
33
- }
34
- } else {
35
- fail('k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES')
36
- }
68
+ for (const r of workspaceRoots) {
69
+ await checkWorkspacePackage(r, fail)
37
70
  }
38
71
 
39
72
  return exitCode
@@ -1,10 +1,98 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
 
4
5
  import { pass } from './utils/pass.mjs'
6
+ import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
5
7
 
6
8
  /**
7
- * Перевіряє відповідність проєкту правилам vue.mdc
9
+ * Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
10
+ * @param {string} rootDir відносний шлях (`'.'` або `site` тощо)
11
+ * @returns {string} підпис для логів перевірки
12
+ */
13
+ function packageLabel(rootDir) {
14
+ return rootDir === '.' ? 'корінь' : rootDir
15
+ }
16
+
17
+ /**
18
+ * Перевіряє залежності та vite.config одного Vue-пакета.
19
+ * @param {string} rootDir відносний шлях до пакета
20
+ * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
21
+ * @returns {Promise<void>} завершується після перевірок залежностей і Vite
22
+ */
23
+ async function checkVuePackage(rootDir, fail) {
24
+ const label = packageLabel(rootDir)
25
+ const prefix = `[${label}] `
26
+
27
+ const pkgPath = join(rootDir, 'package.json')
28
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
29
+ const deps = pkg.dependencies || {}
30
+ const devDeps = pkg.devDependencies || {}
31
+ const allDeps = { ...deps, ...devDeps }
32
+
33
+ if (deps.vue) {
34
+ pass(`${prefix}vue в dependencies: ${deps.vue}`)
35
+ } else {
36
+ fail(`${prefix}vue відсутній в dependencies`)
37
+ }
38
+
39
+ if (devDeps.vite) {
40
+ const match = devDeps.vite.match(/(\d+)/)
41
+ if (match && Number(match[1]) >= 8) {
42
+ pass(`${prefix}vite >= 8: ${devDeps.vite}`)
43
+ } else {
44
+ fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
45
+ }
46
+ } else {
47
+ fail(`${prefix}vite відсутній в devDependencies`)
48
+ }
49
+
50
+ if (devDeps['@vitejs/plugin-vue']) {
51
+ pass(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
52
+ } else {
53
+ fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
54
+ }
55
+
56
+ if (allDeps['vue-macros']) {
57
+ pass(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
58
+ } else {
59
+ fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
60
+ }
61
+
62
+ if (allDeps['unplugin-auto-import']) {
63
+ pass(`${prefix}unplugin-auto-import присутній`)
64
+ } else {
65
+ fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
66
+ }
67
+
68
+ if (allDeps['vite-plugin-vue-layouts-next']) {
69
+ pass(`${prefix}vite-plugin-vue-layouts-next присутній`)
70
+ } else {
71
+ fail(`${prefix}vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next`)
72
+ }
73
+
74
+ const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
75
+ const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
76
+ if (viteConfig) {
77
+ const relConfig = join(rootDir, viteConfig)
78
+ const content = await readFile(relConfig, 'utf8')
79
+ if (content.includes('VueMacros')) {
80
+ pass(`${prefix}${viteConfig} використовує VueMacros`)
81
+ } else {
82
+ fail(`${prefix}${viteConfig} не містить VueMacros`)
83
+ }
84
+ if (content.includes('AutoImport')) {
85
+ pass(`${prefix}${viteConfig} використовує AutoImport`)
86
+ } else {
87
+ fail(`${prefix}${viteConfig} не містить AutoImport`)
88
+ }
89
+ } else {
90
+ fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
8
96
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
9
97
  */
10
98
  export async function check() {
@@ -25,70 +113,26 @@ export async function check() {
25
113
  fail('.vscode/extensions.json не існує')
26
114
  }
27
115
 
28
- if (existsSync('package.json')) {
29
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
30
- const deps = pkg.dependencies || {}
31
- const devDeps = pkg.devDependencies || {}
32
- const allDeps = { ...deps, ...devDeps }
33
-
34
- if (deps.vue) {
35
- pass(`vue в dependencies: ${deps.vue}`)
36
- } else {
37
- fail('vue відсутній в dependencies')
38
- }
39
-
40
- if (devDeps.vite) {
41
- const match = devDeps.vite.match(/(\d+)/)
42
- if (match && Number(match[1]) >= 8) {
43
- pass(`vite >= 8: ${devDeps.vite}`)
44
- } else {
45
- fail(`vite має бути >= 8, знайдено: ${devDeps.vite}`)
46
- }
47
- } else {
48
- fail('vite відсутній в devDependencies')
49
- }
50
-
51
- if (devDeps['@vitejs/plugin-vue']) {
52
- pass(`@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
53
- } else {
54
- fail('@vitejs/plugin-vue відсутній в devDependencies')
55
- }
56
-
57
- if (allDeps['vue-macros']) {
58
- pass(`vue-macros: ${allDeps['vue-macros']}`)
59
- } else {
60
- fail('vue-macros відсутній — bun add -d vue-macros')
61
- }
62
-
63
- if (allDeps['unplugin-auto-import']) {
64
- pass('unplugin-auto-import присутній')
65
- } else {
66
- fail('unplugin-auto-import відсутній — bun add -d unplugin-auto-import')
116
+ const roots = await getMonorepoPackageRootDirs()
117
+ /** @type {string[]} */
118
+ const vueRoots = []
119
+ for (const r of roots) {
120
+ const p = join(r, 'package.json')
121
+ if (existsSync(p)) {
122
+ const pkg = JSON.parse(await readFile(p, 'utf8'))
123
+ if (pkg.dependencies?.vue) vueRoots.push(r)
67
124
  }
125
+ }
68
126
 
69
- if (allDeps['vite-plugin-vue-layouts-next']) {
70
- pass('vite-plugin-vue-layouts-next присутній')
71
- } else {
72
- fail('vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next')
73
- }
127
+ if (vueRoots.length === 0) {
128
+ fail(
129
+ 'vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)'
130
+ )
131
+ return exitCode
74
132
  }
75
133
 
76
- const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
77
- const viteConfig = configFiles.find(f => existsSync(f))
78
- if (viteConfig) {
79
- const content = await readFile(viteConfig, 'utf8')
80
- if (content.includes('VueMacros')) {
81
- pass('vite.config використовує VueMacros')
82
- } else {
83
- fail(`${viteConfig} не містить VueMacros`)
84
- }
85
- if (content.includes('AutoImport')) {
86
- pass('vite.config використовує AutoImport')
87
- } else {
88
- fail(`${viteConfig} не містить AutoImport`)
89
- }
90
- } else {
91
- fail('vite.config.js не існує')
134
+ for (const r of vueRoots) {
135
+ await checkVuePackage(r, fail)
92
136
  }
93
137
 
94
138
  return exitCode
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { glob, readFile } from 'node:fs/promises'
3
+ import { dirname, join, relative } from 'node:path'
4
+
5
+ /**
6
+ * Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
7
+ * @param {unknown} workspaces значення `workspaces` з кореневого package.json
8
+ * @returns {string[]} масив патернів workspaces
9
+ */
10
+ export function normalizeWorkspacePatterns(workspaces) {
11
+ if (!workspaces) return []
12
+ if (Array.isArray(workspaces)) return workspaces
13
+ if (typeof workspaces === 'object' && workspaces !== null && Array.isArray(workspaces.packages)) {
14
+ return workspaces.packages
15
+ }
16
+ return []
17
+ }
18
+
19
+ /**
20
+ * Повертає каталоги з `package.json`: корінь репозиторію та всі пакети з `workspaces`.
21
+ * @param {string} repoRoot зазвичай `process.cwd()`
22
+ * @returns {Promise<string[]>} відносні шляхи до коренів пакетів; `'.'` першим, без дублікатів
23
+ */
24
+ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
25
+ const roots = new Set(['.'])
26
+ const rootPkgPath = join(repoRoot, 'package.json')
27
+ if (!existsSync(rootPkgPath)) {
28
+ return ['.']
29
+ }
30
+ const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
31
+ for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
32
+ const w = raw.replaceAll('\\', '/').replace(/\/+$/, '') || '.'
33
+ if (w.includes('*')) {
34
+ const globPat = `${w}/package.json`
35
+ for await (const f of glob(globPat, { cwd: repoRoot })) {
36
+ const abs = join(repoRoot, f)
37
+ const rel = relative(repoRoot, dirname(abs))
38
+ roots.add(rel === '' ? '.' : rel)
39
+ }
40
+ } else {
41
+ const pkgJson = join(repoRoot, w, 'package.json')
42
+ if (existsSync(pkgJson)) roots.add(w)
43
+ }
44
+ }
45
+ const list = [...roots]
46
+ list.sort((a, b) => {
47
+ if (a === '.') return -1
48
+ if (b === '.') return 1
49
+ return a.localeCompare(b)
50
+ })
51
+ return list
52
+ }