@nitra/cursor 1.8.81 → 1.8.84

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Репозиторій `@nitra/cursor` містить cursor-правила у директорії `mdc/`. CLI копіює обрані правила з **каталогу `mdc/` того пакету, з якого виконується `bin/n-cursor.js`**: після `npm i` / `bun add` це зазвичай `node_modules/@nitra/cursor/mdc`; при **`npx @nitra/cursor`** пакет потрапляє в **кеш npx/npm**, і правила читаються з тієї розпакованої копії (у корені проєкту залежність не обов’язкова). Жодних окремих HTTP-запитів до CDN для файлів правил немає — лише те, що вже є в tarball пакету.
8
8
 
9
- Наприклад, правило `mdc/js-format.mdc` буде збережено як `.cursor/rules/n-js-format.mdc`.
9
+ Наприклад, правило `mdc/text.mdc` буде збережено як `.cursor/rules/n-text.mdc`.
10
10
 
11
11
  ## Підготовка
12
12
 
@@ -15,19 +15,18 @@
15
15
  ```json
16
16
  {
17
17
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
18
- "rules": ["js-format", "npm-module", "text"],
18
+ "rules": ["npm-module", "text"],
19
19
  "skills": ["fix"]
20
20
  }
21
21
  ```
22
22
 
23
23
  Доступні правила:
24
24
 
25
- | Назва | Опис |
26
- | ------------ | ------------------------------------------------- |
27
- | `js-format` | Правила форматування JavaScript ecosystem (oxfmt) |
28
- | `npm-module` | Структура репозиторію для npm-модуля (bun mono) |
29
- | `text` | Текстові файли: cspell, CI |
30
- | `k8s` | Kubernetes YAML, Kustomize, kubeconform |
25
+ | Назва | Опис |
26
+ | ------------ | ----------------------------------------------- |
27
+ | `npm-module` | Структура репозиторію для npm-модуля (bun mono) |
28
+ | `text` | Текст, oxfmt, cspell, markdownlint, v8r, CI |
29
+ | `k8s` | Kubernetes YAML, Kustomize, kubeconform |
31
30
 
32
31
  Щоб використовувати конкретну версію правил, оновіть залежність `@nitra/cursor` у проєкті (`bun add -d @nitra/cursor@<версія>` тощо). Поле `version` у `.n-cursor.json`, якщо воно лишилось у старих конфігах, **ігнорується**.
33
32
 
@@ -71,13 +70,12 @@ CLI автоматично (команда завантаження правил
71
70
  ```
72
71
  🔧 @nitra/cursor — завантаження cursor-правил
73
72
 
74
- 📋 Правил до завантаження: 3
75
- ⬇ js-format → .cursor/rules/n-js-format.mdc ... ✅
73
+ 📋 Правил до завантаження: 2
76
74
  ⬇ npm-module → .cursor/rules/n-npm-module.mdc ... ✅
77
75
  ⬇ text → .cursor/rules/n-text.mdc ... ✅
78
76
  📝 Оновлено AGENTS.md з AGENTS.template.md
79
77
 
80
- ✨ Готово: 3 завантажено, 0 з помилками
78
+ ✨ Готово: 2 завантажено, 0 з помилками
81
79
  ```
82
80
 
83
81
  ## Структура пакету
@@ -86,7 +84,6 @@ CLI автоматично (команда завантаження правил
86
84
  npm/
87
85
  ├── AGENTS.template.md # шаблон AGENTS.md для цільових репозиторіїв (потрапляє в npm-архів)
88
86
  ├── mdc/ # cursor-правила (без префікса n-; після синку — .cursor/rules/n-<id>.mdc)
89
- │ ├── js-format.mdc
90
87
  │ ├── npm-module.mdc
91
88
  │ └── text.mdc
92
89
  ├── skills/ # skills (каталоги <id>/; після синку — .cursor/skills/n-<id>/)
package/bin/n-cursor.js CHANGED
@@ -32,6 +32,10 @@
32
32
  *
33
33
  * Якщо в корені є package.json і в ньому ще немає \@nitra/cursor у devDependencies (і не оголошено
34
34
  * в dependencies), CLI дописує devDependencies з діапазоном ^<version> поточного пакету — зручно після npx.
35
+ *
36
+ * Перед копіюванням правил (режим без підкоманди): оновлення \@nitra/cursor у package.json до
37
+ * останньої версії з npm (крім workspace:/file:/link: тощо), `bun i`, далі файли беруться з
38
+ * `node_modules/@nitra/cursor`, якщо пакет з’явився після встановлення.
35
39
  */
36
40
 
37
41
  import { existsSync } from 'node:fs'
@@ -40,10 +44,8 @@ import { basename, dirname, join } from 'node:path'
40
44
  import { cwd } from 'node:process'
41
45
  import { fileURLToPath } from 'node:url'
42
46
 
43
- import {
44
- ensureNitraCursorInRootDevDependencies,
45
- readBundledPackageVersion
46
- } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
47
+ import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
48
+ import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
47
49
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
48
50
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
49
51
 
@@ -68,17 +70,18 @@ const BUNDLED_PACKAGE_ROOT = join(binDir, '..')
68
70
 
69
71
  /**
70
72
  * Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
73
+ * @param {string} [bundledMdcDir] каталог `mdc/` у корені пакету (за замовчуванням — з поточного процесу)
71
74
  * @returns {Promise<string[]>} відсортовані імена файлів правил без суфікса .mdc
72
75
  */
73
- async function discoverBundledRuleNames() {
74
- if (!existsSync(BUNDLED_MDC_DIR)) {
76
+ async function discoverBundledRuleNames(bundledMdcDir = BUNDLED_MDC_DIR) {
77
+ if (!existsSync(bundledMdcDir)) {
75
78
  throw new Error(
76
79
  `Не знайдено каталог правил пакету.\n` +
77
- `Очікуваний шлях: ${BUNDLED_MDC_DIR}\n` +
80
+ `Очікуваний шлях: ${bundledMdcDir}\n` +
78
81
  `Перевстановіть ${PACKAGE_NAME} або створіть ${CONFIG_FILE} вручну.`
79
82
  )
80
83
  }
81
- const names = await readdir(BUNDLED_MDC_DIR)
84
+ const names = await readdir(bundledMdcDir)
82
85
  const rules = names
83
86
  .filter(n => n.endsWith('.mdc'))
84
87
  .map(n => n.slice(0, -'.mdc'.length))
@@ -91,13 +94,14 @@ async function discoverBundledRuleNames() {
91
94
 
92
95
  /**
93
96
  * Імена skills (id без префікса n-) з каталогу skills пакету — лише підкаталоги `<id>/` без префікса n-
97
+ * @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету
94
98
  * @returns {Promise<string[]>} відсортовані id
95
99
  */
96
- async function discoverBundledSkillNames() {
97
- if (!existsSync(BUNDLED_SKILLS_DIR)) {
100
+ async function discoverBundledSkillNames(bundledSkillsDir = BUNDLED_SKILLS_DIR) {
101
+ if (!existsSync(bundledSkillsDir)) {
98
102
  return []
99
103
  }
100
- const entries = await readdir(BUNDLED_SKILLS_DIR, { withFileTypes: true })
104
+ const entries = await readdir(bundledSkillsDir, { withFileTypes: true })
101
105
  return entries
102
106
  .filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith(RULE_PREFIX))
103
107
  .map(e => e.name)
@@ -152,14 +156,17 @@ async function migrateLegacyConfigIfNeeded() {
152
156
 
153
157
  /**
154
158
  * Зчитує конфіг .n-cursor.json з поточної директорії
159
+ * @param {{ bundledMdcDir?: string, bundledSkillsDir?: string }} [paths] каталоги з пакету-джерела (після `bun i` — зазвичай `node_modules/@nitra/cursor`)
155
160
  * @returns {Promise<{ $schema: string, rules: string[], skills: string[], version?: string } & Record<string, unknown>>} rules, skills (id без префікса n-); поле version у файлі за наявності ігнорується при синхронізації правил
156
161
  */
157
- async function readConfig() {
162
+ async function readConfig(paths = {}) {
163
+ const bundledMdcDir = paths.bundledMdcDir ?? BUNDLED_MDC_DIR
164
+ const bundledSkillsDir = paths.bundledSkillsDir ?? BUNDLED_SKILLS_DIR
158
165
  await migrateLegacyConfigIfNeeded()
159
166
  const configPath = join(cwd(), CONFIG_FILE)
160
167
  if (!existsSync(configPath)) {
161
- const rules = await discoverBundledRuleNames()
162
- const skills = await discoverBundledSkillNames()
168
+ const rules = await discoverBundledRuleNames(bundledMdcDir)
169
+ const skills = await discoverBundledSkillNames(bundledSkillsDir)
163
170
  const defaultConfig = { $schema: CONFIG_SCHEMA_URL, rules, skills }
164
171
  await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
165
172
  console.log(
@@ -181,7 +188,7 @@ async function readConfig() {
181
188
  if ('skills' in config) {
182
189
  throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
183
190
  }
184
- config.skills = await discoverBundledSkillNames()
191
+ config.skills = await discoverBundledSkillNames(bundledSkillsDir)
185
192
  }
186
193
 
187
194
  if (config.$schema !== CONFIG_SCHEMA_URL) {
@@ -197,8 +204,8 @@ async function readConfig() {
197
204
 
198
205
  /**
199
206
  * Витягує чисте ім'я файлу правила (без шляху, але зберігає .mdc)
200
- * "npm/mdc/js-format.mdc" → "js-format.mdc"
201
- * "js-format" → "js-format.mdc"
207
+ * "npm/mdc/text.mdc" → "text.mdc"
208
+ * "text" → "text.mdc"
202
209
  * @param {string} ruleName шлях або базове ім'я, з суфіксом .mdc або без
203
210
  * @returns {string} лише ім'я файлу з суфіксом .mdc
204
211
  */
@@ -210,14 +217,15 @@ function normalizeRuleName(ruleName) {
210
217
  /**
211
218
  * Читає вміст правила з каталогу `mdc/` установленого пакету (наприклад `node_modules/@nitra/cursor/mdc` або кеш npx).
212
219
  * @param {string} rule елемент масиву rules з `.n-cursor.json`
220
+ * @param {string} [bundledMdcDir] каталог `mdc/` у корені пакету-джерела
213
221
  * @returns {Promise<string>} текст правила для запису в `.cursor/rules/n-*.mdc`
214
222
  */
215
- function readBundledRuleContent(rule) {
223
+ function readBundledRuleContent(rule, bundledMdcDir = BUNDLED_MDC_DIR) {
216
224
  const bundledName = normalizeRuleName(rule)
217
- const bundledPath = join(BUNDLED_MDC_DIR, bundledName)
225
+ const bundledPath = join(bundledMdcDir, bundledName)
218
226
  if (!existsSync(bundledPath)) {
219
227
  throw new Error(
220
- `Немає файлу ${bundledName} у ${BUNDLED_MDC_DIR}. Оновіть ${PACKAGE_NAME} або приберіть "${rule}" з rules у ${CONFIG_FILE}.`
228
+ `Немає файлу ${bundledName} у ${bundledMdcDir}. Оновіть ${PACKAGE_NAME} або приберіть "${rule}" з rules у ${CONFIG_FILE}.`
221
229
  )
222
230
  }
223
231
  return readFile(bundledPath, 'utf8')
@@ -457,17 +465,18 @@ async function syncClaudeMd(configRules, configSkills) {
457
465
  /**
458
466
  * Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
459
467
  * @param {string[]} configSkills id skills з конфігу
468
+ * @param {string} [agentsTemplatePath] шлях до AGENTS.template.md у корені пакету-джерела
460
469
  * @returns {Promise<void>} завершення запису файлу
461
470
  */
462
- async function syncAgentsMd(configSkills) {
463
- if (!existsSync(BUNDLED_AGENTS_TEMPLATE_PATH)) {
471
+ async function syncAgentsMd(configSkills, agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
472
+ if (!existsSync(agentsTemplatePath)) {
464
473
  throw new Error(
465
474
  `Не знайдено шаблон ${AGENTS_TEMPLATE_FILE} у пакеті.\n` +
466
- `Очікуваний шлях: ${BUNDLED_AGENTS_TEMPLATE_PATH}\n` +
475
+ `Очікуваний шлях: ${agentsTemplatePath}\n` +
467
476
  `Перевстановіть ${PACKAGE_NAME}.`
468
477
  )
469
478
  }
470
- const templateText = await readFile(BUNDLED_AGENTS_TEMPLATE_PATH, 'utf8')
479
+ const templateText = await readFile(agentsTemplatePath, 'utf8')
471
480
  const mdcFiles = await listProjectRulesMdcFiles()
472
481
  const skillItems = await buildSkillBulletItems(configSkills)
473
482
  const body = renderAgentsTemplate(templateText, mdcFiles, skillItems)
@@ -485,10 +494,11 @@ async function syncAgentsMd(configSkills) {
485
494
  /**
486
495
  * Копіює лише skills зі списку configSkills (джерело: skills/<id>/ у пакеті)
487
496
  * @param {string[]} configSkills id без префікса n-
497
+ * @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету-джерела
488
498
  * @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих копіювань
489
499
  */
490
- async function syncSkills(configSkills) {
491
- if (configSkills.length === 0 || !existsSync(BUNDLED_SKILLS_DIR)) {
500
+ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
501
+ if (configSkills.length === 0 || !existsSync(bundledSkillsDir)) {
492
502
  return { success: 0, fail: 0 }
493
503
  }
494
504
 
@@ -500,7 +510,7 @@ async function syncSkills(configSkills) {
500
510
 
501
511
  for (const skillId of configSkills) {
502
512
  const id = normalizeSkillId(skillId)
503
- const srcDir = join(BUNDLED_SKILLS_DIR, id)
513
+ const srcDir = join(bundledSkillsDir, id)
504
514
  const destDirName = managedSkillDirName(skillId)
505
515
  const destDir = join(skillsRoot, destDirName)
506
516
 
@@ -534,10 +544,11 @@ async function syncSkills(configSkills) {
534
544
  * Синхронізує .claude/commands/n-<id>.md зі skills пакету.
535
545
  * Кожен файл містить посилання на відповідний cursor skill, а не копію інструкцій.
536
546
  * @param {string[]} configSkills id без префікса n-
547
+ * @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету-джерела
537
548
  * @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
538
549
  */
539
- async function syncCommands(configSkills) {
540
- if (configSkills.length === 0 || !existsSync(BUNDLED_SKILLS_DIR)) {
550
+ async function syncCommands(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
551
+ if (configSkills.length === 0 || !existsSync(bundledSkillsDir)) {
541
552
  return { success: 0, fail: 0 }
542
553
  }
543
554
 
@@ -549,7 +560,7 @@ async function syncCommands(configSkills) {
549
560
 
550
561
  for (const skillId of configSkills) {
551
562
  const id = normalizeSkillId(skillId)
552
- const srcSkillMd = join(BUNDLED_SKILLS_DIR, id, 'SKILL.md')
563
+ const srcSkillMd = join(bundledSkillsDir, id, 'SKILL.md')
553
564
  const destDirName = managedSkillDirName(skillId)
554
565
  const destFile = join(commandsDir, `${RULE_PREFIX}${id}.md`)
555
566
 
@@ -730,6 +741,24 @@ async function runChecks(requestedRules) {
730
741
  }
731
742
  }
732
743
 
744
+ /**
745
+ * Читає поле `version` з `package.json` пакету за абсолютним шляхом до його кореня.
746
+ * @param {string} packageRoot корінь пакету (тека з `package.json`)
747
+ * @returns {Promise<string | null>} semver рядком або null, якщо файлу/поля немає або JSON некоректний
748
+ */
749
+ async function readBundledVersionAt(packageRoot) {
750
+ const p = join(packageRoot, 'package.json')
751
+ if (!existsSync(p)) {
752
+ return null
753
+ }
754
+ try {
755
+ const pkg = JSON.parse(await readFile(p, 'utf8'))
756
+ return typeof pkg.version === 'string' ? pkg.version : null
757
+ } catch {
758
+ return null
759
+ }
760
+ }
761
+
733
762
  /**
734
763
  * Копіює правила з каталогу `mdc/` установленого пакету та синхронізує `.cursor/rules`
735
764
  * @returns {Promise<void>}
@@ -737,18 +766,36 @@ async function runChecks(requestedRules) {
737
766
  async function runSync() {
738
767
  console.log(`\n🔧 ${PACKAGE_NAME} — завантаження cursor-правил\n`)
739
768
 
769
+ const projectRoot = cwd()
770
+ let effectivePackageRoot
771
+ try {
772
+ effectivePackageRoot = await upgradeNitraCursorToLatestAndBunInstall(projectRoot, BUNDLED_PACKAGE_ROOT)
773
+ } catch (error) {
774
+ const msg = error instanceof Error ? error.message : String(error)
775
+ console.error(`❌ Не вдалося оновити ${PACKAGE_NAME} або виконати bun i: ${msg}`)
776
+ throw error
777
+ }
778
+
779
+ const bundledMdcDir = join(effectivePackageRoot, 'mdc')
780
+ const bundledSkillsDir = join(effectivePackageRoot, 'skills')
781
+ const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
782
+
740
783
  let config
741
784
  try {
742
- config = await readConfig()
785
+ config = await readConfig({ bundledMdcDir, bundledSkillsDir })
743
786
  } catch (error) {
744
787
  console.error(`❌ ${error.message}`)
745
788
  throw error
746
789
  }
747
790
 
748
791
  const { rules, skills, version } = config
749
- const bundledVer = await readBundledPackageVersion()
792
+ const bundledVer = await readBundledVersionAt(effectivePackageRoot)
750
793
  if (bundledVer) {
751
- console.log(`📦 Джерело правил: ${PACKAGE_NAME}@${bundledVer}`)
794
+ const line =
795
+ effectivePackageRoot === BUNDLED_PACKAGE_ROOT
796
+ ? `📦 Джерело правил: ${PACKAGE_NAME}@${bundledVer}`
797
+ : `📦 Джерело правил: ${PACKAGE_NAME}@${bundledVer} (шлях: ${effectivePackageRoot})`
798
+ console.log(`${line}\n`)
752
799
  }
753
800
  if (version) {
754
801
  console.log(`⚠️ Поле "version" у ${CONFIG_FILE} ігнорується; правила беруться з установленого пакету.\n`)
@@ -757,7 +804,7 @@ async function runSync() {
757
804
  console.log(`📋 Skills до синхронізації: ${skills.length}`)
758
805
 
759
806
  try {
760
- const { destPath } = await syncSetupBunDepsAction(cwd(), BUNDLED_PACKAGE_ROOT)
807
+ const { destPath } = await syncSetupBunDepsAction(cwd(), effectivePackageRoot)
761
808
  console.log(`📝 Оновлено ${destPath} (composite setup-bun-deps з пакету)\n`)
762
809
  } catch (error) {
763
810
  console.error(`❌ Не вдалося записати setup-bun-deps action: ${error.message}`)
@@ -776,7 +823,7 @@ async function runSync() {
776
823
 
777
824
  try {
778
825
  process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
779
- const content = await readBundledRuleContent(rule)
826
+ const content = await readBundledRuleContent(rule, bundledMdcDir)
780
827
  await writeFile(destPath, content, 'utf8')
781
828
  console.log(`✅`)
782
829
  successCount++
@@ -801,7 +848,7 @@ async function runSync() {
801
848
  }
802
849
 
803
850
  try {
804
- const { success: skillOk, fail: skillFail } = await syncSkills(skills)
851
+ const { success: skillOk, fail: skillFail } = await syncSkills(skills, bundledSkillsDir)
805
852
  if (skills.length > 0) {
806
853
  console.log(`\n🧩 Skills: ${skillOk} скопійовано, ${skillFail} з помилками`)
807
854
  }
@@ -821,7 +868,7 @@ async function runSync() {
821
868
  }
822
869
 
823
870
  try {
824
- const { success: cmdOk, fail: cmdFail } = await syncCommands(skills)
871
+ const { success: cmdOk, fail: cmdFail } = await syncCommands(skills, bundledSkillsDir)
825
872
  if (skills.length > 0) {
826
873
  console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
827
874
  }
@@ -841,7 +888,7 @@ async function runSync() {
841
888
  }
842
889
 
843
890
  try {
844
- await syncAgentsMd(skills)
891
+ await syncAgentsMd(skills, bundledAgentsTemplatePath)
845
892
  } catch (error) {
846
893
  console.error(`❌ Не вдалося оновити ${AGENTS_FILE}: ${error.message}`)
847
894
  throw error
package/mdc/abie.mdc CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.8'
4
+ version: '1.9'
5
5
  ---
6
6
 
7
- Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), а також гілки **dev**, **ua**, **ru** у **clean-merged-branch**.
7
+ Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
8
8
 
9
9
  **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
10
 
@@ -135,6 +135,10 @@ spec:
135
135
  preem: 'true' # буде замінено через kustomize
136
136
  ```
137
137
 
138
+ ## Firebase Hosting
139
+
140
+ У корені репозиторію не тримати конфіг і кеш **Firebase Hosting**: видали **`.firebaserc`**, **`firebase.json`** та каталог **`.firebase/`**, якщо вони є.
141
+
138
142
  ## Git branches
139
143
 
140
144
  У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно):
package/mdc/k8s.mdc CHANGED
@@ -106,6 +106,88 @@ resources: {}
106
106
 
107
107
  Образ **`hasura/graphql-engine`**: дозволений лише канонічний тег із константи **`HASURA_GRAPHQL_ENGINE_IMAGE`** у **`check-k8s.mjs`** (допускається префікс **`docker.io/`**); решта — помилка **check k8s**.
108
108
 
109
+ ### HTTPRoute для Deployment з `hasura/graphql-engine`
110
+
111
+ Якщо для такого **Deployment** описано **`HTTPRoute`**, **`spec.rules`** мають відповідати канону нижче: редірект **`/ql`** та **`/ql/`** на **`/ql/console`** (**`302`**), правило з **`PathPrefix` `/ql`**, **`URLRewrite`** префікса на **`/`**, окреме правило для **WebSocket** (**`Upgrade: websocket`**) з **`RequestHeaderModifier`** (прибрати **`Authorization`** — авторизація для WebSocket іде всередині messages), далі **`PathPrefix` `/`** на той самий backend.
112
+
113
+ **`parentRefs`**, **`hostnames`**, **`metadata.namespace`** / **`name`** підлаштуй під середовище. У **`backendRefs.name`** вказуй **headless** **Service** з суфіксом **`-hl`** (див. розділ **«Service: `svc.yaml` і `svc-hl.yaml`»**); у прикладі **`db-h-hl`** заміни на фактичне ім’я.
114
+
115
+ ```yaml
116
+ # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/gateway.networking.k8s.io/httproute_v1beta1.json
117
+ apiVersion: gateway.networking.k8s.io/v1beta1
118
+ kind: HTTPRoute
119
+ metadata:
120
+ name: db-h
121
+ namespace: dev
122
+ spec:
123
+ parentRefs:
124
+ - name: gw
125
+ namespace: dev
126
+ sectionName: https
127
+ hostnames:
128
+ - abie.cloud
129
+ rules:
130
+ - matches:
131
+ - path:
132
+ type: Exact
133
+ value: /ql
134
+ filters:
135
+ - type: RequestRedirect
136
+ requestRedirect:
137
+ path:
138
+ type: ReplaceFullPath
139
+ replaceFullPath: /ql/console
140
+ statusCode: 302
141
+ - matches:
142
+ - path:
143
+ type: Exact
144
+ value: /ql/
145
+ filters:
146
+ - type: RequestRedirect
147
+ requestRedirect:
148
+ path:
149
+ type: ReplaceFullPath
150
+ replaceFullPath: /ql/console
151
+ statusCode: 302
152
+ - matches:
153
+ - path:
154
+ type: PathPrefix
155
+ value: /ql
156
+ filters:
157
+ - type: URLRewrite
158
+ urlRewrite:
159
+ path:
160
+ type: ReplacePrefixMatch
161
+ replacePrefixMatch: /
162
+ backendRefs:
163
+ - name: db-h-hl
164
+ port: 8080
165
+ # У WebSocket авторизація йде всередині messages
166
+ - matches:
167
+ - path:
168
+ type: PathPrefix
169
+ value: /
170
+ headers:
171
+ - type: Exact
172
+ name: Upgrade
173
+ value: websocket
174
+ filters:
175
+ - type: RequestHeaderModifier
176
+ requestHeaderModifier:
177
+ remove:
178
+ - Authorization
179
+ backendRefs:
180
+ - name: db-h-hl
181
+ port: 8080
182
+ - matches:
183
+ - path:
184
+ type: PathPrefix
185
+ value: /
186
+ backendRefs:
187
+ - name: db-h-hl
188
+ port: 8080
189
+ ```
190
+
109
191
  ## Service: заборонені анотації GKE
110
192
 
111
193
  У **`kind: Service`** не додавай у **`metadata.annotations`** **`cloud.google.com/neg`** і **`cloud.google.com/backend-config`** (legacy під Ingress / старе балансування GKE). **check k8s** падає, якщо ключ є.
@@ -224,7 +306,7 @@ patches:
224
306
 
225
307
  **`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
226
308
 
227
- **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
309
+ **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), повна структура **`HTTPRoute`** для **Hasura** (канон — у розділі про **`hasura/graphql-engine`**), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
228
310
 
229
311
  ## Коли застосовувати (агентам)
230
312
 
@@ -7,7 +7,7 @@ version: '1.2'
7
7
  ## Генерація та редагування стилів (Cursor і інші агенти)
8
8
 
9
9
  - **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
10
- - **Форматування** узгоджуй з **`n-js-format.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
10
+ - **Форматування** узгоджуй з **`n-text.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
11
11
  - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
12
12
  - **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
13
13
 
package/mdc/text.mdc CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
- description: Обробка та перевірка текстових файлів (cspell, markdownlint-cli2, v8r, CI)
2
+ description: Обробка та перевірка текстових файлів, oxfmt, cspell, markdownlint-cli2, v8r, CI
3
3
  alwaysApply: true
4
- version: '1.24'
4
+ version: '1.25'
5
5
  ---
6
6
 
7
- **cspell**, **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint**, workflow **`lint-text`**.
7
+ **oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint**, workflow **`lint-text`**.
8
8
 
9
9
  ```json title=".vscode/extensions.json"
10
10
  {
@@ -24,10 +24,92 @@ version: '1.24'
24
24
  "files.associations": {
25
25
  "*.env.*": "env",
26
26
  "*.env": "env"
27
- }
27
+ },
28
+ "editor.formatOnSave": true,
29
+ "[css]": {
30
+ "editor.defaultFormatter": "oxc.oxc-vscode"
31
+ },
32
+ "[graphql]": {
33
+ "editor.defaultFormatter": "oxc.oxc-vscode"
34
+ },
35
+ "[handlebars]": {
36
+ "editor.defaultFormatter": "oxc.oxc-vscode"
37
+ },
38
+ "[html]": {
39
+ "editor.defaultFormatter": "oxc.oxc-vscode"
40
+ },
41
+ "[javascript]": {
42
+ "editor.defaultFormatter": "oxc.oxc-vscode"
43
+ },
44
+ "[json]": {
45
+ "editor.defaultFormatter": "oxc.oxc-vscode"
46
+ },
47
+ "[json5]": {
48
+ "editor.defaultFormatter": "oxc.oxc-vscode"
49
+ },
50
+ "[jsonc]": {
51
+ "editor.defaultFormatter": "oxc.oxc-vscode"
52
+ },
53
+ "[less]": {
54
+ "editor.defaultFormatter": "oxc.oxc-vscode"
55
+ },
56
+ "[markdown]": {
57
+ "editor.defaultFormatter": "oxc.oxc-vscode"
58
+ },
59
+ "[mdx]": {
60
+ "editor.defaultFormatter": "oxc.oxc-vscode"
61
+ },
62
+ "[scss]": {
63
+ "editor.defaultFormatter": "oxc.oxc-vscode"
64
+ },
65
+ "[toml]": {
66
+ "editor.defaultFormatter": "oxc.oxc-vscode"
67
+ },
68
+ "[typescript]": {
69
+ "editor.defaultFormatter": "oxc.oxc-vscode"
70
+ },
71
+ "[vue]": {
72
+ "editor.defaultFormatter": "oxc.oxc-vscode"
73
+ },
74
+ "[yaml]": {
75
+ "editor.defaultFormatter": "oxc.oxc-vscode"
76
+ },
77
+ "oxc.path.oxfmt": "/opt/homebrew/bin/oxfmt"
28
78
  }
29
79
  ```
30
80
 
81
+ У корені проєкту має бути файл з правилами форматування для **oxfmt**:
82
+
83
+ ```json title=".oxfmtrc.json"
84
+ {
85
+ "ignorePatterns": ["**/hasura/metadata/**", "**/schema.graphql"],
86
+ "arrowParens": "avoid",
87
+ "printWidth": 120,
88
+ "bracketSpacing": true,
89
+ "bracketSameLine": true,
90
+ "embeddedLanguageFormatting": "auto",
91
+ "endOfLine": "lf",
92
+ "htmlWhitespaceSensitivity": "css",
93
+ "insertPragma": false,
94
+ "jsxSingleQuote": true,
95
+ "proseWrap": "preserve",
96
+ "quoteProps": "as-needed",
97
+ "requirePragma": false,
98
+ "semi": false,
99
+ "singleQuote": true,
100
+ "tabWidth": 2,
101
+ "trailingComma": "none",
102
+ "useTabs": false,
103
+ "vueIndentScriptAndStyle": false
104
+ }
105
+ ```
106
+
107
+ Поле **`ignorePatterns`** додавай за потреби (наприклад згенеровані каталоги); якщо виключень немає — ключ можна опустити.
108
+
109
+ Також потрібно прибрати, якщо є в проєкті, модуль **`@nitra/prettier-config`**, **prettier** та всі виклики prettier і налаштування для нього.
110
+
111
+ Завжди пиши **JSDoc** до функцій та методів.
112
+
31
113
  **`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**. Для української додай **`@cspell/dict-uk-ua`**. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`bunx v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
32
114
 
33
115
  У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` скрипт підставляє в v8r сам. За бажання можна передати власні glob-и аргументами скрипта. Шлях до скрипта: `./npm/scripts/…`, `./scripts/…` після копіювання, або `node_modules/@nitra/cursor/scripts/…`.
@@ -215,4 +297,4 @@ jobs:
215
297
 
216
298
  ## Перевірка
217
299
 
218
- `npx @nitra/cursor check text`
300
+ `npx @nitra/cursor check text` (охоплює oxfmt, cspell, markdownlint, v8r, CI для `lint-text`)
package/mdc/vue.mdc CHANGED
@@ -108,7 +108,7 @@ const additionalInstructions = `
108
108
 
109
109
  ### Інструменти (узгоджено з Vite і цим правилом)
110
110
 
111
- - Якість коду: **ESLint** + **eslint-plugin-vue**; форматування — **oxfmt**, див. `js-format.mdc`.
111
+ - Якість коду: **ESLint** + **eslint-plugin-vue**; форматування — **oxfmt**, див. `text.mdc`.
112
112
  - Збірка та dev-сервер — **Vite**
113
113
  - **Vue Devtools** для дебагу; продакшен-збірка — **`vite build`**, оптимізація асетів і кешування на рівні деплою / CDN.
114
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.81",
3
+ "version": "1.8.84",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -8,6 +8,8 @@
8
8
  * **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
9
9
  * **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно).
10
10
  *
11
+ * **Firebase Hosting:** у корені репозиторію не має бути **`.firebaserc`**, **`firebase.json`** та каталогу **`.firebase/`**.
12
+ *
11
13
  * **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
12
14
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
13
15
  * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
@@ -968,6 +970,30 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
968
970
  }
969
971
  }
970
972
 
973
+ /**
974
+ * Перевіряє відсутність артефактів Firebase Hosting у корені репозиторію (abie.mdc).
975
+ * @param {string} root корінь репозиторію
976
+ * @param {(msg: string) => void} passFn успішне повідомлення
977
+ * @param {(msg: string) => void} failFn повідомлення про порушення
978
+ * @returns {void}
979
+ */
980
+ function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
981
+ for (const name of ['.firebaserc', 'firebase.json']) {
982
+ const abs = join(root, name)
983
+ if (existsSync(abs)) {
984
+ failFn(`Знайдено заборонений файл Firebase Hosting: ${name} — видали його (abie.mdc)`)
985
+ } else {
986
+ passFn(`Немає ${name}`)
987
+ }
988
+ }
989
+ const firebaseDir = join(root, '.firebase')
990
+ if (existsSync(firebaseDir)) {
991
+ failFn('Знайдено директорію .firebase — видали її (abie.mdc)')
992
+ } else {
993
+ passFn('Немає .firebase/')
994
+ }
995
+ }
996
+
971
997
  /**
972
998
  * Перевіряє відповідність проєкту правилам abie.mdc.
973
999
  * @returns {Promise<number>} 0 — OK, 1 — є порушення
@@ -984,6 +1010,7 @@ export async function check() {
984
1010
  }
985
1011
 
986
1012
  pass('Правило abie увімкнено — виконуємо перевірки')
1013
+ ensureNoFirebaseHostingArtifacts(root, pass, fail)
987
1014
 
988
1015
  const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
989
1016
  if (existsSync(cleanMergedPath)) {
@@ -47,6 +47,8 @@
47
47
  * **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
48
48
  * компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
49
49
  * Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
50
+ *
51
+ * Структура **`HTTPRoute`** для **Deployment** з образом **`hasura/graphql-engine`** (редиректи **`/ql`**, **WebSocket**, **`URLRewrite`**) — лише в **k8s.mdc**, автоматично не звіряється.
50
52
  */
51
53
  import { existsSync } from 'node:fs'
52
54
  import { readFile, stat, unlink } from 'node:fs/promises'
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Перевіряє текстовий стек за правилом text.mdc.
2
+ * Перевіряє текстовий стек і форматування за правилом text.mdc.
3
+ *
4
+ * oxfmt: `.oxfmtrc.json` з обовʼязковими ключами, VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
5
+ * відсутність Prettier у конфігах і залежностях.
3
6
  *
4
7
  * cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json), заборона
5
8
  * `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
6
9
  * `.v8rignore` (vscode JSON),
7
- * workflow `lint-text.yml`, розширення VSCode для markdownlint.
10
+ * workflow `lint-text.yml`, розширення VSCode (markdownlint, oxc).
8
11
  *
9
12
  * Якщо є `.cursor/rules/n-text.mdc` і/або `npm/mdc/text.mdc` — перевіряє наявність абзацу про український
10
13
  * апостроф (U+0027 vs U+2019) і приклад з символом U+2019 у тексті.
@@ -43,7 +46,7 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
43
46
  }
44
47
 
45
48
  /**
46
- * Перевіряє відповідність проєкту правилам text.mdc (cspell, markdownlint через bunx, v8r)
49
+ * Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, markdownlint через bunx, v8r)
47
50
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
48
51
  */
49
52
  export async function check() {
@@ -79,6 +82,11 @@ export async function check() {
79
82
  } else {
80
83
  fail('extensions.json: додай "DavidAnson.vscode-markdownlint" у recommendations (див. n-text.mdc)')
81
84
  }
85
+ if (Array.isArray(rec) && rec.includes('oxc.oxc-vscode')) {
86
+ pass('extensions.json містить oxc.oxc-vscode')
87
+ } else {
88
+ fail('extensions.json: додай "oxc.oxc-vscode" у recommendations (див. n-text.mdc)')
89
+ }
82
90
  } catch {
83
91
  fail('.vscode/extensions.json — невалідний JSON')
84
92
  }
@@ -86,6 +94,63 @@ export async function check() {
86
94
  fail('.vscode/extensions.json не існує — створи з recommendations згідно n-text.mdc')
87
95
  }
88
96
 
97
+ if (existsSync('.vscode/settings.json')) {
98
+ try {
99
+ const settings = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
100
+ if (settings['editor.formatOnSave'] === true) {
101
+ pass('settings.json: editor.formatOnSave увімкнено')
102
+ } else {
103
+ fail('settings.json: editor.formatOnSave має бути true')
104
+ }
105
+ const fmtTypes = ['javascript', 'typescript', 'json', 'vue', 'css', 'html']
106
+ for (const t of fmtTypes) {
107
+ const key = `[${t}]`
108
+ if (settings[key]?.['editor.defaultFormatter'] === 'oxc.oxc-vscode') {
109
+ pass(`settings.json: ${key} використовує oxc.oxc-vscode`)
110
+ } else {
111
+ fail(`settings.json: ${key} має використовувати oxc.oxc-vscode як defaultFormatter`)
112
+ }
113
+ }
114
+ } catch {
115
+ fail('.vscode/settings.json — невалідний JSON')
116
+ }
117
+ } else {
118
+ fail('.vscode/settings.json не існує — створи згідно n-text.mdc')
119
+ }
120
+
121
+ const expectedOxfmtKeys = [
122
+ 'arrowParens',
123
+ 'printWidth',
124
+ 'bracketSpacing',
125
+ 'bracketSameLine',
126
+ 'semi',
127
+ 'singleQuote',
128
+ 'tabWidth',
129
+ 'trailingComma',
130
+ 'useTabs'
131
+ ]
132
+ if (existsSync('.oxfmtrc.json')) {
133
+ const cfg = JSON.parse(await readFile('.oxfmtrc.json', 'utf8'))
134
+ const missing = expectedOxfmtKeys.filter(k => !(k in cfg))
135
+ if (missing.length === 0) {
136
+ pass('.oxfmtrc.json містить всі обовʼязкові ключі')
137
+ } else {
138
+ fail(`.oxfmtrc.json відсутні ключі: ${missing.join(', ')}`)
139
+ }
140
+ if (cfg.semi !== false) fail('.oxfmtrc.json: semi має бути false')
141
+ if (cfg.singleQuote !== true) fail('.oxfmtrc.json: singleQuote має бути true')
142
+ if (cfg.tabWidth !== 2) fail('.oxfmtrc.json: tabWidth має бути 2')
143
+ if (cfg.useTabs !== false) fail('.oxfmtrc.json: useTabs має бути false')
144
+ if (cfg.printWidth !== 120) fail('.oxfmtrc.json: printWidth має бути 120')
145
+ } else {
146
+ fail('.oxfmtrc.json не існує — створи його')
147
+ }
148
+
149
+ const prettierFiles = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']
150
+ for (const f of prettierFiles) {
151
+ if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
152
+ }
153
+
89
154
  if (existsSync('.markdownlint-cli2.jsonc')) {
90
155
  try {
91
156
  const ml = JSON.parse(await readFile('.markdownlint-cli2.jsonc', 'utf8'))
@@ -145,6 +210,12 @@ export async function check() {
145
210
 
146
211
  if (existsSync('package.json')) {
147
212
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
213
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
214
+ for (const dep of ['prettier', '@nitra/prettier-config']) {
215
+ if (allDeps[dep]) fail(`package.json містить залежність ${dep} — видали її`)
216
+ }
217
+ if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
218
+
148
219
  const devDeps = pkg.devDependencies || {}
149
220
 
150
221
  if (devDeps['@nitra/cspell-dict']) {
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Перед синхронізацією правил CLI підтягує останню опубліковану версію `@nitra/cursor` з npm registry
3
+ * у кореневий `package.json` і запускає `bun i` у корені проєкту.
4
+ *
5
+ * Якщо залежність уже задана через `workspace:`, `file:`, `link:` тощо, запис у registry не
6
+ * змінюється і `bun i` не викликається — так зберігається робота монорепо та сценаріїв з `workspace:`, `file:` чи `link:`.
7
+ *
8
+ * Після встановлення повертається шлях до `node_modules/@nitra/cursor`, якщо каталог з
9
+ * `package.json` існує; інакше — fallback (корінь пакету поточного процесу CLI, наприклад кеш npx).
10
+ */
11
+
12
+ import { execFile } from 'node:child_process'
13
+ import { promisify } from 'node:util'
14
+ import { existsSync } from 'node:fs'
15
+ import { readFile, writeFile } from 'node:fs/promises'
16
+ import { join } from 'node:path'
17
+
18
+ const PACKAGE_NAME = '@nitra/cursor'
19
+ const NPM_LATEST_URL = 'https://registry.npmjs.org/@nitra/cursor/latest'
20
+
21
+ const execFileAsync = promisify(execFile)
22
+
23
+ /**
24
+ * Чи не можна безпечно підставити semver з npm замість поточного специфікатора залежності.
25
+ * @param {string} specifier значення з package.json
26
+ * @returns {boolean} true — залишити як є (монорепо, git, tarball тощо)
27
+ */
28
+ export function shouldSkipNpmVersionUpgrade(specifier) {
29
+ const s = String(specifier).trim()
30
+ if (!s) {
31
+ return true
32
+ }
33
+ if (/^workspace:/i.test(s)) {
34
+ return true
35
+ }
36
+ if (/^file:/i.test(s)) {
37
+ return true
38
+ }
39
+ if (/^link:/i.test(s)) {
40
+ return true
41
+ }
42
+ if (/^portal:/i.test(s)) {
43
+ return true
44
+ }
45
+ if (/^git(\+|:\/\/)/i.test(s)) {
46
+ return true
47
+ }
48
+ if (/^npm:/i.test(s)) {
49
+ return true
50
+ }
51
+ if (/^https?:\/\//i.test(s)) {
52
+ return true
53
+ }
54
+ if (s.startsWith('./') || s.startsWith('../')) {
55
+ return true
56
+ }
57
+ return false
58
+ }
59
+
60
+ /**
61
+ * Остання версія пакета з npm (поле `version` у JSON dist-tag `latest`).
62
+ * @returns {Promise<string>} semver без префікса `^`
63
+ */
64
+ export async function fetchLatestNitraCursorVersionFromNpm() {
65
+ const res = await fetch(NPM_LATEST_URL, {
66
+ headers: { accept: 'application/json' }
67
+ })
68
+ if (!res.ok) {
69
+ throw new Error(`npm registry: ${res.status} ${res.statusText} для ${PACKAGE_NAME}`)
70
+ }
71
+ const data = await res.json()
72
+ if (!data || typeof data.version !== 'string' || !data.version.trim()) {
73
+ throw new Error(`npm registry: у відповіді для ${PACKAGE_NAME} немає поля version`)
74
+ }
75
+ return data.version.trim()
76
+ }
77
+
78
+ /**
79
+ * Шлях до встановленого пакета в `node_modules` або fallback.
80
+ * @param {string} projectRoot корінь репозиторію
81
+ * @param {string} fallbackPackageRoot корінь пакету з поточного процесу
82
+ * @returns {string} абсолютний шлях до каталогу з `mdc/`, `scripts/` тощо
83
+ */
84
+ export function resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot) {
85
+ const installed = join(projectRoot, 'node_modules', PACKAGE_NAME)
86
+ if (existsSync(join(installed, 'package.json'))) {
87
+ return installed
88
+ }
89
+ return fallbackPackageRoot
90
+ }
91
+
92
+ /**
93
+ * Запускає `bun i` у вказаному каталозі з виводом у поточний stdio.
94
+ * @param {string} projectRoot cwd для процесу
95
+ * @returns {Promise<void>} завершується після успішного `bun i`
96
+ */
97
+ async function runBunInstall(projectRoot) {
98
+ try {
99
+ await execFileAsync('bun', ['i'], { cwd: projectRoot, stdio: 'inherit' })
100
+ } catch (error) {
101
+ const exitCode = typeof error?.code === 'number' ? error.code : null
102
+ if (exitCode !== null && exitCode !== 0) {
103
+ throw new Error(`bun i завершився з кодом ${exitCode}`)
104
+ }
105
+ throw error
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Де зараз оголошено `@nitra/cursor` у package.json.
111
+ * @param {Record<string, unknown>} pkg вміст package.json як об'єкт після читання JSON
112
+ * @returns {{ section: 'devDependencies' | 'dependencies', value: string } | null} секція та специфікатор або null, якщо залежності немає
113
+ */
114
+ function findNitraCursorDependency(pkg) {
115
+ const dev = pkg.devDependencies
116
+ if (dev && typeof dev === 'object' && !Array.isArray(dev) && PACKAGE_NAME in dev) {
117
+ const value = dev[PACKAGE_NAME]
118
+ if (typeof value === 'string') {
119
+ return { section: 'devDependencies', value }
120
+ }
121
+ }
122
+ const deps = pkg.dependencies
123
+ if (deps && typeof deps === 'object' && !Array.isArray(deps) && PACKAGE_NAME in deps) {
124
+ const value = deps[PACKAGE_NAME]
125
+ if (typeof value === 'string') {
126
+ return { section: 'dependencies', value }
127
+ }
128
+ }
129
+ return null
130
+ }
131
+
132
+ /**
133
+ * Оновлює `@nitra/cursor` до `^<latest>` з npm (якщо дозволено специфікатором), виконує `bun i`,
134
+ * повертає корінь пакету для читання `mdc/` та інших файлів синхронізації.
135
+ * @param {string} projectRoot корінь цільового репозиторію (`cwd()`)
136
+ * @param {string} fallbackPackageRoot корінь пакету з `import.meta.url` (кеш npx або workspace)
137
+ * @returns {Promise<string>} абсолютний шлях до кореня `@nitra/cursor` для копіювання файлів
138
+ */
139
+ export async function upgradeNitraCursorToLatestAndBunInstall(projectRoot, fallbackPackageRoot) {
140
+ const pkgPath = join(projectRoot, 'package.json')
141
+ if (!existsSync(pkgPath)) {
142
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
143
+ }
144
+
145
+ let raw
146
+ try {
147
+ raw = await readFile(pkgPath, 'utf8')
148
+ } catch {
149
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
150
+ }
151
+
152
+ let pkg
153
+ try {
154
+ pkg = JSON.parse(raw)
155
+ } catch {
156
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
157
+ }
158
+
159
+ if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) {
160
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
161
+ }
162
+
163
+ const found = findNitraCursorDependency(pkg)
164
+
165
+ if (found && shouldSkipNpmVersionUpgrade(found.value)) {
166
+ console.log(`⏭️ ${PACKAGE_NAME}: специфікатор «${found.value}» — без оновлення з npm та без bun i\n`)
167
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
168
+ }
169
+
170
+ const latest = await fetchLatestNitraCursorVersionFromNpm()
171
+ const desired = `^${latest}`
172
+
173
+ if (!found) {
174
+ if (!pkg.devDependencies || typeof pkg.devDependencies !== 'object' || Array.isArray(pkg.devDependencies)) {
175
+ pkg.devDependencies = {}
176
+ }
177
+ pkg.devDependencies[PACKAGE_NAME] = desired
178
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
179
+ console.log(`📝 Додано ${PACKAGE_NAME}@${desired} у devDependencies (остання з npm)\n`)
180
+ await runBunInstall(projectRoot)
181
+ console.log(`📦 Виконано bun i у корені проєкту\n`)
182
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
183
+ }
184
+
185
+ if (found.value === desired) {
186
+ console.log(`📌 ${PACKAGE_NAME} уже ${desired} у package.json — виконуємо bun i\n`)
187
+ } else {
188
+ if (found.section === 'devDependencies') {
189
+ pkg.devDependencies[PACKAGE_NAME] = desired
190
+ } else {
191
+ pkg.dependencies[PACKAGE_NAME] = desired
192
+ }
193
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
194
+ console.log(`📝 Оновлено ${PACKAGE_NAME} → ${desired} у package.json\n`)
195
+ }
196
+
197
+ await runBunInstall(projectRoot)
198
+ console.log(`📦 Виконано bun i у корені проєкту\n`)
199
+
200
+ return resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot)
201
+ }
package/mdc/js-format.mdc DELETED
@@ -1,103 +0,0 @@
1
- ---
2
- description: Правила форматів JavaScript ecosystem
3
- alwaysApply: true
4
- version: '1.3'
5
- ---
6
-
7
- В корені проекту є файл з правилами форматування для oxfmt:
8
-
9
- ```json title=".oxfmtrc.json"
10
- {
11
- "arrowParens": "avoid",
12
- "printWidth": 120,
13
- "bracketSpacing": true,
14
- "bracketSameLine": true,
15
- "embeddedLanguageFormatting": "auto",
16
- "endOfLine": "lf",
17
- "htmlWhitespaceSensitivity": "css",
18
- "insertPragma": false,
19
- "jsxSingleQuote": true,
20
- "proseWrap": "preserve",
21
- "quoteProps": "as-needed",
22
- "requirePragma": false,
23
- "semi": false,
24
- "singleQuote": true,
25
- "tabWidth": 2,
26
- "trailingComma": "none",
27
- "useTabs": false,
28
- "vueIndentScriptAndStyle": false
29
- }
30
- ```
31
-
32
- в файлі .vscode/extensions.json є налаштування для oxfmt:
33
-
34
- ```json title=".vscode/extensions.json"
35
- {
36
- "recommendations": ["oxc.oxc-vscode"]
37
- }
38
- ```
39
-
40
- в файлі .vscode/settings.json є налаштування для oxfmt:
41
-
42
- ```json title=".vscode/settings.json"
43
- {
44
- "editor.formatOnSave": true,
45
- "[css]": {
46
- "editor.defaultFormatter": "oxc.oxc-vscode"
47
- },
48
- "[graphql]": {
49
- "editor.defaultFormatter": "oxc.oxc-vscode"
50
- },
51
- "[handlebars]": {
52
- "editor.defaultFormatter": "oxc.oxc-vscode"
53
- },
54
- "[html]": {
55
- "editor.defaultFormatter": "oxc.oxc-vscode"
56
- },
57
- "[javascript]": {
58
- "editor.defaultFormatter": "oxc.oxc-vscode"
59
- },
60
- "[json]": {
61
- "editor.defaultFormatter": "oxc.oxc-vscode"
62
- },
63
- "[json5]": {
64
- "editor.defaultFormatter": "oxc.oxc-vscode"
65
- },
66
- "[jsonc]": {
67
- "editor.defaultFormatter": "oxc.oxc-vscode"
68
- },
69
- "[less]": {
70
- "editor.defaultFormatter": "oxc.oxc-vscode"
71
- },
72
- "[markdown]": {
73
- "editor.defaultFormatter": "oxc.oxc-vscode"
74
- },
75
- "[mdx]": {
76
- "editor.defaultFormatter": "oxc.oxc-vscode"
77
- },
78
- "[scss]": {
79
- "editor.defaultFormatter": "oxc.oxc-vscode"
80
- },
81
- "[toml]": {
82
- "editor.defaultFormatter": "oxc.oxc-vscode"
83
- },
84
- "[typescript]": {
85
- "editor.defaultFormatter": "oxc.oxc-vscode"
86
- },
87
- "[vue]": {
88
- "editor.defaultFormatter": "oxc.oxc-vscode"
89
- },
90
- "[yaml]": {
91
- "editor.defaultFormatter": "oxc.oxc-vscode"
92
- },
93
- "oxc.path.oxfmt": "/opt/homebrew/bin/oxfmt"
94
- }
95
- ```
96
-
97
- Також потрібно прибрати якщо є в проекті модул @nitra/prettier-config та prettier та всі виклики prettier і налаштування для нього.
98
-
99
- Завжди пиши JSDoc до функцій та методів.
100
-
101
- ## Перевірка
102
-
103
- `npx @nitra/cursor check js-format`
@@ -1,96 +0,0 @@
1
- /**
2
- * Перевіряє форматування коду за правилом js-format.mdc.
3
- *
4
- * `.oxfmtrc.json` з потрібними ключами, VSCode і oxfmt, відсутність Prettier у конфігах і залежностях.
5
- */
6
- import { existsSync } from 'node:fs'
7
- import { readFile } from 'node:fs/promises'
8
-
9
- import { createCheckReporter } from './utils/check-reporter.mjs'
10
-
11
- /**
12
- * Перевіряє відповідність проєкту правилам js-format.mdc
13
- * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
14
- */
15
- export async function check() {
16
- const reporter = createCheckReporter()
17
- const { pass, fail } = reporter
18
-
19
- const expectedKeys = [
20
- 'arrowParens',
21
- 'printWidth',
22
- 'bracketSpacing',
23
- 'bracketSameLine',
24
- 'semi',
25
- 'singleQuote',
26
- 'tabWidth',
27
- 'trailingComma',
28
- 'useTabs'
29
- ]
30
-
31
- if (existsSync('.oxfmtrc.json')) {
32
- const cfg = JSON.parse(await readFile('.oxfmtrc.json', 'utf8'))
33
- const missing = expectedKeys.filter(k => !(k in cfg))
34
- if (missing.length === 0) {
35
- pass('.oxfmtrc.json містить всі обовʼязкові ключі')
36
- } else {
37
- fail(`.oxfmtrc.json відсутні ключі: ${missing.join(', ')}`)
38
- }
39
-
40
- if (cfg.semi !== false) fail('.oxfmtrc.json: semi має бути false')
41
- if (cfg.singleQuote !== true) fail('.oxfmtrc.json: singleQuote має бути true')
42
- if (cfg.tabWidth !== 2) fail('.oxfmtrc.json: tabWidth має бути 2')
43
- if (cfg.useTabs !== false) fail('.oxfmtrc.json: useTabs має бути false')
44
- if (cfg.printWidth !== 120) fail('.oxfmtrc.json: printWidth має бути 120')
45
- } else {
46
- fail('.oxfmtrc.json не існує — створи його')
47
- }
48
-
49
- if (existsSync('.vscode/extensions.json')) {
50
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
51
- if (ext.recommendations?.includes('oxc.oxc-vscode')) {
52
- pass('extensions.json містить oxc.oxc-vscode')
53
- } else {
54
- fail('extensions.json не містить oxc.oxc-vscode')
55
- }
56
- } else {
57
- fail('.vscode/extensions.json не існує')
58
- }
59
-
60
- if (existsSync('.vscode/settings.json')) {
61
- const settings = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
62
- if (settings['editor.formatOnSave'] === true) {
63
- pass('settings.json: editor.formatOnSave увімкнено')
64
- } else {
65
- fail('settings.json: editor.formatOnSave має бути true')
66
- }
67
-
68
- const fmtTypes = ['javascript', 'typescript', 'json', 'vue', 'css', 'html']
69
- for (const t of fmtTypes) {
70
- const key = `[${t}]`
71
- if (settings[key]?.['editor.defaultFormatter'] === 'oxc.oxc-vscode') {
72
- pass(`settings.json: ${key} використовує oxc.oxc-vscode`)
73
- } else {
74
- fail(`settings.json: ${key} має використовувати oxc.oxc-vscode як defaultFormatter`)
75
- }
76
- }
77
- } else {
78
- fail('.vscode/settings.json не існує')
79
- }
80
-
81
- const prettierFiles = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']
82
- for (const f of prettierFiles) {
83
- if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
84
- }
85
-
86
- if (existsSync('package.json')) {
87
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
88
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
89
- for (const dep of ['prettier', '@nitra/prettier-config']) {
90
- if (allDeps[dep]) fail(`package.json містить залежність ${dep} — видали її`)
91
- }
92
- if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
93
- }
94
-
95
- return reporter.getExitCode()
96
- }