@nitra/cursor 1.8.110 → 1.8.111

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/bin/n-cursor.js CHANGED
@@ -221,6 +221,10 @@ async function readConfig(paths = {}) {
221
221
  if ('skills' in parsedConfig && !Array.isArray(parsedConfig.skills)) {
222
222
  throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
223
223
  }
224
+ const { ignore } = parsedConfig
225
+ if (ignore !== undefined && (!Array.isArray(ignore) || ignore.some(p => typeof p !== 'string'))) {
226
+ throw new Error(`У ${CONFIG_FILE} поле "ignore" має бути масивом рядків (шляхів до директорій)`)
227
+ }
224
228
 
225
229
  const rootPkg = await readRootPackageJsonSafe()
226
230
  const disableRules = normalizeIdList(parsedConfig['disable-rules'])
@@ -571,10 +575,23 @@ async function buildClaudeSkillsSectionLines() {
571
575
  * Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
572
576
  * @returns {Promise<void>}
573
577
  */
574
- async function syncClaudeMd() {
578
+ /**
579
+ * @param {string[]} [ignore] директорії заборонені для редагування
580
+ */
581
+ async function syncClaudeMd(ignore) {
575
582
  const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
576
- const mdcFiles = await listProjectRulesMdcFiles()
577
583
 
584
+ if (Array.isArray(ignore) && ignore.length > 0) {
585
+ lines.push('## Захищені директорії', '', 'Ніколи не змінюй, не видаляй і не створюй файли у цих директоріях:')
586
+ for (const dir of ignore) {
587
+ let d = dir
588
+ while (d.endsWith('/')) d = d.slice(0, -1)
589
+ lines.push(`- \`${d}/\``)
590
+ }
591
+ lines.push('')
592
+ }
593
+
594
+ const mdcFiles = await listProjectRulesMdcFiles()
578
595
  for (const mdcFile of mdcFiles) {
579
596
  lines.push(`@${RULES_DIR}/${mdcFile}`)
580
597
  }
@@ -971,7 +988,7 @@ async function runSync() {
971
988
 
972
989
  const config = await runSyncStep('❌ ', () => readConfig({ bundledMdcDir, bundledSkillsDir }))
973
990
 
974
- const { rules, skills, version } = config
991
+ const { rules, skills, version, ignore } = config
975
992
  const bundledVer = await readBundledVersionAt(effectivePackageRoot)
976
993
  if (bundledVer) {
977
994
  const line =
@@ -1025,7 +1042,9 @@ async function runSync() {
1025
1042
  })
1026
1043
 
1027
1044
  await runSyncStep(`❌ Не вдалося оновити ${AGENTS_FILE}: `, () => syncAgentsMd(bundledAgentsTemplatePath))
1028
- await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () => syncClaudeMd())
1045
+ await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () =>
1046
+ syncClaudeMd(/** @type {string[] | undefined} */ (ignore))
1047
+ )
1029
1048
 
1030
1049
  console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
1031
1050
  if (failCount > 0) {
package/mdc/abie.mdc CHANGED
@@ -113,6 +113,148 @@ spec:
113
113
  gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
114
114
  ```
115
115
 
116
+ ## k8s: overlay **ru** і nginx-sidecar для WebSocket (Hasura)
117
+
118
+ YC ALB (gwin) має баг: якщо HTTPRoute-правило містить одночасно `URLRewrite` (ReplacePrefixMatch) і `upgrade_types: websocket` — ALB не обробляє WebSocket і повертає 404.
119
+ https://center.yandex.cloud/support/tickets/TX549394
120
+
121
+ Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і проксіює WebSocket до Hasura.
122
+
123
+ **Умова:** в дереві `k8s` є Deployment з image `hasura/graphql-engine` (або `newName` на нього через `images:` у kustomization) **і** `ru/kustomization.yaml` містить `HASURA_GRAPHQL_JWT_SECRET` (patch на ConfigMap Hasura з JWT).
124
+
125
+ ### Що потрібно зробити
126
+
127
+ **1. `ru/configmap-nginx.yaml`** — ConfigMap з nginx.conf:
128
+
129
+ ```yaml title="…/k8s/ru/configmap-nginx.yaml"
130
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/configmap-v1.json
131
+ apiVersion: v1
132
+ kind: ConfigMap
133
+ metadata:
134
+ name: СЕРВІС-nginx
135
+ namespace: ru-NAMESPACE
136
+ data:
137
+ nginx.conf: |
138
+ user nginx;
139
+ worker_processes auto;
140
+ error_log /dev/null;
141
+
142
+ events {
143
+ worker_connections 1024;
144
+ }
145
+
146
+ http {
147
+ access_log off;
148
+
149
+ map $http_upgrade $connection_upgrade {
150
+ default upgrade;
151
+ '' close;
152
+ }
153
+
154
+ server {
155
+ listen 8081;
156
+ server_name _;
157
+
158
+ location /healthz {
159
+ add_header Content-Type text/plain;
160
+ return 200 "healthy";
161
+ }
162
+
163
+ # WebSocket: ALB не робить rewrite (URLRewrite + upgrade = bug YC ALB)
164
+ # nginx реврайтить /PREFIX/* → /* і проксіює до Hasura
165
+ location /PREFIX/ {
166
+ rewrite ^/PREFIX/(.*)$ /$1 break;
167
+ proxy_pass http://127.0.0.1:8080;
168
+ proxy_http_version 1.1;
169
+ proxy_set_header Upgrade $http_upgrade;
170
+ proxy_set_header Connection $connection_upgrade;
171
+ proxy_set_header Host $host;
172
+ }
173
+
174
+ # HTTP: ALB вже зробив prefix_rewrite → /, nginx просто проксіює
175
+ location / {
176
+ proxy_pass http://127.0.0.1:8080;
177
+ proxy_http_version 1.1;
178
+ proxy_set_header Upgrade $http_upgrade;
179
+ proxy_set_header Connection $connection_upgrade;
180
+ proxy_set_header Host $host;
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ **2. `ru/kustomization.yaml`** — додати до `resources` та чотири patch-блоки:
187
+
188
+ ```yaml title="…/k8s/ru/kustomization.yaml (фрагмент)"
189
+ resources:
190
+ - ../base
191
+ - configmap-nginx.yaml # ← додати
192
+
193
+ patches:
194
+ # Headless Service: замінити ports на два іменованих (hasura:8080 + proxy:8081)
195
+ - target:
196
+ kind: Service
197
+ name: СЕРВІС-hl
198
+ patch: |-
199
+ - op: replace
200
+ path: /spec/type
201
+ value: NodePort
202
+ - op: remove
203
+ path: /spec/clusterIP
204
+ - op: replace
205
+ path: /spec/ports
206
+ value:
207
+ - name: hasura
208
+ protocol: TCP
209
+ port: 8080
210
+ - name: proxy
211
+ protocol: TCP
212
+ port: 8081
213
+
214
+ # Deployment: додати nginx-sidecar контейнер та volume
215
+ - target:
216
+ kind: Deployment
217
+ name: СЕРВІС
218
+ patch: |-
219
+ - op: add
220
+ path: /spec/template/spec/containers/-
221
+ value:
222
+ name: СЕРВІС-p
223
+ image: nginx:1.27-alpine
224
+ ports:
225
+ - containerPort: 8081
226
+ protocol: TCP
227
+ volumeMounts:
228
+ - name: nginx-conf
229
+ mountPath: /etc/nginx/nginx.conf
230
+ subPath: nginx.conf
231
+ resources: {}
232
+ - op: add
233
+ path: /spec/template/spec/volumes
234
+ value:
235
+ - name: nginx-conf
236
+ configMap:
237
+ name: СЕРВІС-nginx
238
+
239
+ # HTTPRoute: прибрати URLRewrite-фільтри з PathPrefix-правила,
240
+ # направити трафік на nginx:8081, видалити мертве WebSocket-правило
241
+ - target:
242
+ kind: HTTPRoute
243
+ name: СЕРВІС
244
+ patch: |-
245
+ - op: remove
246
+ path: /spec/rules/INDEX_PATHPREFIX/filters
247
+ - op: replace
248
+ path: /spec/rules/INDEX_PATHPREFIX/backendRefs/0/port
249
+ value: 8081
250
+ - op: remove
251
+ path: /spec/rules/INDEX_WEBSOCKET
252
+ ```
253
+
254
+ > `INDEX_PATHPREFIX` і `INDEX_WEBSOCKET` — індекси правил у `base/hr.yaml`. Після видалення filters ALB більше не додає `prefix_rewrite`, тому nginx-sidecar отримує оригінальний шлях і сам виконує rewrite.
255
+
256
+ **Примітка щодо Service:** якщо в base у `-hl` Service два порти задаються вперше, `spec.ports[*].name` є обов'язковим — без нього `kubectl apply` поверне помилку `Required value`.
257
+
116
258
  ## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
117
259
 
118
260
  Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`**, у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`**. Якщо в base **явно** задано **`spec.clusterIPs`** (зокрема **`['None']`**), додай **`op: remove`** і для **`/spec/clusterIPs`** — інакше **API** може відхилити **NodePort** (*`spec.clusterIPs[0]: Invalid value: "None"`*). **Не** додавай **`remove`** на **`/spec/clusterIPs`**, якщо ключа **немає** в base: **`kubectl kustomize`** тоді падає (*Unable to remove nonexistent key*). Якщо в base лише **`clusterIP: None`**, а помилка лишається після **`kubectl apply -k`** (злиття з уже існуючим **Service** у кластері), тимчасово **видали** **`Service`** у **`ru`** і застосуй знову, або додай у base поле **`clusterIPs`**, щоб **`remove`** у patch був валідний для **kustomize**. Деталі — **`check-abie.mjs`**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.110",
3
+ "version": "1.8.111",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -43,6 +43,14 @@
43
43
  "minLength": 1
44
44
  }
45
45
  },
46
+ "ignore": {
47
+ "type": "array",
48
+ "description": "Директорії, у яких заборонено будь-які модифікації файлів (AI не редагує, не видаляє, не створює файли). Шляхи відносно кореня репозиторію. Наприклад: [\"packages/vendor\", \"packages/generated\"].",
49
+ "items": {
50
+ "type": "string",
51
+ "minLength": 1
52
+ }
53
+ },
46
54
  "version": {
47
55
  "type": "string",
48
56
  "description": "Застаріле поле, ігнорується CLI. Правила завжди копіюються з каталогу mdc/ установленого пакету (node_modules або кеш npx); змініть версію через оновлення залежності."
@@ -2344,12 +2344,7 @@ function findHasuraCanonStart(rules) {
2344
2344
  } else {
2345
2345
  const m = asPlainRecord(matches[0])
2346
2346
  const p = m === null || m.headers !== undefined ? null : asPlainRecord(m.path)
2347
- if (
2348
- p !== null &&
2349
- p.type === 'Exact' &&
2350
- typeof p.value === 'string' &&
2351
- p.value.endsWith('/ql')
2352
- ) {
2347
+ if (p !== null && p.type === 'Exact' && typeof p.value === 'string' && p.value.endsWith('/ql')) {
2353
2348
  return { prefix: p.value.slice(0, -'/ql'.length), startIndex: i }
2354
2349
  }
2355
2350
  }
@@ -2641,7 +2636,7 @@ async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
2641
2636
  * Якщо документ — Hasura-Deployment із непорожнім `metadata.name`, додає ім'я до індексу за каталогом.
2642
2637
  * @param {Record<string, unknown>} rec корінь YAML-документа
2643
2638
  * @param {string} dir абсолютний шлях до каталогу файлу
2644
- * @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (мутується)
2639
+ * @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (під час обходу в нього додаються імена)
2645
2640
  * @returns {void}
2646
2641
  */
2647
2642
  function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
@@ -2677,7 +2672,9 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
2677
2672
  const v = httpRouteHasuraCanonViolation(hr.obj)
2678
2673
  if (v !== null) {
2679
2674
  const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
2680
- fail(`${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`)
2675
+ fail(
2676
+ `${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`
2677
+ )
2681
2678
  }
2682
2679
  }
2683
2680
  }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
6
6
  * Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
7
- * щоб правило працювало й у CommonJS/інлайн-завантаженні.
7
+ * щоб правило працювало й у CommonJS і при динамічному import у межах одного файлу.
8
8
  *
9
9
  * Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
10
10
  * результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.