@nitra/cursor 1.8.109 → 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 +23 -4
- package/mdc/abie.mdc +145 -3
- package/package.json +1 -1
- package/schemas/n-cursor.json +8 -0
- package/scripts/check-abie.mjs +6 -6
- package/scripts/check-bun.mjs +3 -2
- package/scripts/check-js-lint.mjs +38 -19
- package/scripts/check-k8s.mjs +7 -10
- package/scripts/utils/bunyan-imports.mjs +9 -8
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
|
-
|
|
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: ', () =>
|
|
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
|
@@ -4,7 +4,7 @@ alwaysApply: true
|
|
|
4
4
|
version: '1.15'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`
|
|
7
|
+
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`file-link-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **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
|
|
|
@@ -38,7 +38,7 @@ spec:
|
|
|
38
38
|
|
|
39
39
|
За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
40
40
|
|
|
41
|
-
### HTTPRoute: спільні сервіси **`auth-run-hl`**, **`
|
|
41
|
+
### HTTPRoute: спільні сервіси **`auth-run-hl`**, **`file-link-hl`**
|
|
42
42
|
|
|
43
43
|
Ці **Service** (headless **`-hl`**) живуть у **базовому** неймспейсі **`dev`**. У маніфесті **HTTPRoute** під **`k8s`** (шар без **`ua/`** та **`ru/`** — наприклад **`…/k8s/base/hr.yaml`**) для кожного **`backendRefs`** до такого сервісу явно вкажи **`namespace: dev`** і порт **8080**:
|
|
44
44
|
|
|
@@ -53,7 +53,7 @@ spec:
|
|
|
53
53
|
- name: auth-run-hl
|
|
54
54
|
namespace: dev
|
|
55
55
|
port: 8080
|
|
56
|
-
- name:
|
|
56
|
+
- name: file-link-hl
|
|
57
57
|
namespace: dev
|
|
58
58
|
port: 8080
|
|
59
59
|
```
|
|
@@ -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
package/schemas/n-cursor.json
CHANGED
|
@@ -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); змініть версію через оновлення залежності."
|
package/scripts/check-abie.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
30
30
|
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
|
|
31
31
|
* якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
|
|
32
|
-
* **Спільні бекенди (`auth-run-hl`, `
|
|
32
|
+
* **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** та **ru** (шлях не містить **`k8s/ua/`** чи **`k8s/ru/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
|
|
33
33
|
* у patch overlay **ua** та **ru** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** або **ru** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
|
|
34
34
|
* Вибір **`op`** — **k8s.mdc**.
|
|
35
35
|
*
|
|
@@ -56,7 +56,7 @@ const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
|
|
|
56
56
|
* Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов'язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
|
|
57
57
|
* Експорт для споживачів / тестів.
|
|
58
58
|
*/
|
|
59
|
-
export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', '
|
|
59
|
+
export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'file-link-hl'])
|
|
60
60
|
|
|
61
61
|
const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
|
|
62
62
|
|
|
@@ -985,7 +985,7 @@ function checkSharedBackendRef(br, rel, errors) {
|
|
|
985
985
|
}
|
|
986
986
|
|
|
987
987
|
/**
|
|
988
|
-
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`
|
|
988
|
+
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`file-link-hl`** і порушення **`namespace: dev`**.
|
|
989
989
|
* @param {unknown} obj корінь YAML
|
|
990
990
|
* @param {string} rel відносний шлях (повідомлення)
|
|
991
991
|
* @returns {{ refCount: number, errors: string[] }} кількість посилань і список порушень
|
|
@@ -1015,7 +1015,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
1017
|
/**
|
|
1018
|
-
* З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`
|
|
1018
|
+
* З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
|
|
1019
1019
|
* @param {string} root корінь репозиторію
|
|
1020
1020
|
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1021
1021
|
* @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
|
|
@@ -1135,7 +1135,7 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
1135
1135
|
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
1136
1136
|
* @param {'ua' | 'ru'} mode **ua** або **ru**
|
|
1137
1137
|
* @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
|
|
1138
|
-
* @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`
|
|
1138
|
+
* @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
|
|
1139
1139
|
* @returns {string | null} повідомлення про помилку або **null**
|
|
1140
1140
|
*/
|
|
1141
1141
|
export function validateAbieNginxRunHttpRoutePatches(
|
|
@@ -1173,7 +1173,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1173
1173
|
if (sharedCount > 0) {
|
|
1174
1174
|
const patchHits = countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode)
|
|
1175
1175
|
if (patchHits < sharedCount) {
|
|
1176
|
-
return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl,
|
|
1176
|
+
return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl, file-link-hl очікується ${sharedCount} JSON6902 patch(ів) з path /spec/rules/…/backendRefs/…/namespace та value ${mode} (зараз ${patchHits}) — abie.mdc`
|
|
1177
1177
|
}
|
|
1178
1178
|
}
|
|
1179
1179
|
return null
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -21,8 +21,9 @@ import { readFile } from 'node:fs/promises'
|
|
|
21
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
22
|
|
|
23
23
|
const OXFMT_END_RE = /&&[ \t]+oxfmt[ \t]+\.[ \t]*$/
|
|
24
|
-
|
|
25
|
-
const
|
|
24
|
+
/** Пробіли/таби без `\s` (уникаємо super-linear backtracking у sonarjs/slow-regex). */
|
|
25
|
+
const HOISTED_LINKER_RE = /^[ \t]*linker[ \t]*=[ \t]*"hoisted"[ \t]*$/m
|
|
26
|
+
const INSTALL_SECTION_RE = /^[ \t]*\[install\][ \t]*$/m
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Перевіряє `bunfig.toml` на секцію `[install]` з `linker = "hoisted"`.
|
|
@@ -183,6 +183,42 @@ function checkPackageJsonTypeModule(label, pkg, passFn, failFn) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* `"type": "module"` у кожного workspace з package.json.
|
|
188
|
+
* @param {unknown[]} workspaces поле workspaces з package.json
|
|
189
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
190
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
191
|
+
*/
|
|
192
|
+
async function checkWorkspacePackagesTypeModule(workspaces, passFn, failFn) {
|
|
193
|
+
for (const ws of workspaces) {
|
|
194
|
+
const wsPkgPath = `${ws}/package.json`
|
|
195
|
+
if (existsSync(wsPkgPath)) {
|
|
196
|
+
const wsPkg = JSON.parse(await readFile(wsPkgPath, 'utf8'))
|
|
197
|
+
checkPackageJsonTypeModule(wsPkgPath, wsPkg, passFn, failFn)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* engines.node >= 24.
|
|
204
|
+
* @param {{ engines?: { node?: string } }} pkg розпарсений package.json
|
|
205
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
206
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
207
|
+
*/
|
|
208
|
+
function checkEnginesNode(pkg, passFn, failFn) {
|
|
209
|
+
const nodeEngine = pkg.engines?.node
|
|
210
|
+
if (nodeEngine) {
|
|
211
|
+
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
212
|
+
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
213
|
+
passFn(`engines.node: "${nodeEngine}"`)
|
|
214
|
+
} else {
|
|
215
|
+
failFn(`engines.node: "${nodeEngine}" — має бути >=24`)
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
failFn('package.json не містить engines.node — додай: "engines": { "node": ">=24" }')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
186
222
|
/**
|
|
187
223
|
* Перевіряє package.json на lint-js, prettier, eslint-config, engines.node.
|
|
188
224
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
@@ -195,13 +231,7 @@ async function checkPackageJsonJsLint(passFn, failFn) {
|
|
|
195
231
|
checkPackageJsonTypeModule('package.json', pkg, passFn, failFn)
|
|
196
232
|
|
|
197
233
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
|
|
198
|
-
|
|
199
|
-
const wsPkgPath = `${ws}/package.json`
|
|
200
|
-
if (existsSync(wsPkgPath)) {
|
|
201
|
-
const wsPkg = JSON.parse(await readFile(wsPkgPath, 'utf8'))
|
|
202
|
-
checkPackageJsonTypeModule(wsPkgPath, wsPkg, passFn, failFn)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
234
|
+
await checkWorkspacePackagesTypeModule(workspaces, passFn, failFn)
|
|
205
235
|
|
|
206
236
|
const lintJs = pkg.scripts?.['lint-js']
|
|
207
237
|
if (lintJs) {
|
|
@@ -218,18 +248,7 @@ async function checkPackageJsonJsLint(passFn, failFn) {
|
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
checkPackageJsonLintDeps(pkg, passFn, failFn)
|
|
221
|
-
|
|
222
|
-
const nodeEngine = pkg.engines?.node
|
|
223
|
-
if (nodeEngine) {
|
|
224
|
-
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
225
|
-
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
226
|
-
passFn(`engines.node: "${nodeEngine}"`)
|
|
227
|
-
} else {
|
|
228
|
-
failFn(`engines.node: "${nodeEngine}" — має бути >=24`)
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
failFn('package.json не містить engines.node — додай: "engines": { "node": ">=24" }')
|
|
232
|
-
}
|
|
251
|
+
checkEnginesNode(pkg, passFn, failFn)
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
/**
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -2336,20 +2336,15 @@ function hasuraRuleIsWebsocket(rule, qlPath) {
|
|
|
2336
2336
|
* @returns {{ prefix: string, startIndex: number } | null} виявлений префікс і позиція правила 1 або null
|
|
2337
2337
|
*/
|
|
2338
2338
|
function findHasuraCanonStart(rules) {
|
|
2339
|
-
for (
|
|
2340
|
-
const r = asPlainRecord(
|
|
2339
|
+
for (const [i, rule] of rules.entries()) {
|
|
2340
|
+
const r = asPlainRecord(rule)
|
|
2341
2341
|
const matches = r === null ? null : r.matches
|
|
2342
2342
|
if (!Array.isArray(matches) || matches.length !== 1) {
|
|
2343
2343
|
// наступне правило
|
|
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(
|
|
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
|
* результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
|
|
@@ -61,7 +61,7 @@ function normalizeSnippet(s) {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
|
|
64
|
-
* @param {
|
|
64
|
+
* @param {Record<string, unknown> | null | undefined} node вузол AST
|
|
65
65
|
* @returns {string | null} ім'я модуля з аргументу, інакше `null`
|
|
66
66
|
*/
|
|
67
67
|
function requireCallModule(node) {
|
|
@@ -75,7 +75,7 @@ function requireCallModule(node) {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
|
|
78
|
-
* @param {
|
|
78
|
+
* @param {Record<string, unknown> | null | undefined} node вузол AST
|
|
79
79
|
* @returns {string | null} ім'я модуля, інакше `null`
|
|
80
80
|
*/
|
|
81
81
|
function dynamicImportModule(node) {
|
|
@@ -87,8 +87,8 @@ function dynamicImportModule(node) {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Простий рекурсивний обхід AST: заходимо в усі об'єкти/масиви, щоб знайти require/import-вузли.
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {(n:
|
|
90
|
+
* @param {unknown} node корінь або під-вузол AST
|
|
91
|
+
* @param {(n: unknown) => void} visit виклик для кожного об'єкта-вузла
|
|
92
92
|
* @returns {void}
|
|
93
93
|
*/
|
|
94
94
|
function walkAst(node, visit) {
|
|
@@ -101,9 +101,10 @@ function walkAst(node, visit) {
|
|
|
101
101
|
visit(node)
|
|
102
102
|
}
|
|
103
103
|
for (const key of Object.keys(node)) {
|
|
104
|
-
if (key
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
if (key !== 'parent') {
|
|
105
|
+
const v = node[key]
|
|
106
|
+
if (v && typeof v === 'object') walkAst(v, visit)
|
|
107
|
+
}
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|