@nitra/cursor 1.8.104 → 1.8.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mdc/k8s.mdc CHANGED
@@ -123,7 +123,9 @@ metadata:
123
123
  namespace: dev
124
124
  spec:
125
125
  parentRefs:
126
- - name: gw
126
+ - group: gateway.networking.k8s.io
127
+ kind: Gateway
128
+ name: gw
127
129
  namespace: dev
128
130
  sectionName: https
129
131
  hostnames:
@@ -164,16 +166,22 @@ spec:
164
166
  backendRefs:
165
167
  - name: db-h-hl
166
168
  port: 8080
167
- # У WebSocket авторизація йде всередині messages
169
+ # у websocket авторизація йде всередині messages
170
+ # Той самий URLRewrite, що й для HTTP: інакше бекенд бачить /ql/v1/graphql замість /v1/graphql
168
171
  - matches:
169
172
  - path:
170
173
  type: PathPrefix
171
- value: /
174
+ value: /ql
172
175
  headers:
173
176
  - type: Exact
174
177
  name: Upgrade
175
178
  value: websocket
176
179
  filters:
180
+ - type: URLRewrite
181
+ urlRewrite:
182
+ path:
183
+ type: ReplacePrefixMatch
184
+ replacePrefixMatch: /
177
185
  - type: RequestHeaderModifier
178
186
  requestHeaderModifier:
179
187
  remove:
@@ -181,13 +189,6 @@ spec:
181
189
  backendRefs:
182
190
  - name: db-h-hl
183
191
  port: 8080
184
- - matches:
185
- - path:
186
- type: PathPrefix
187
- value: /
188
- backendRefs:
189
- - name: db-h-hl
190
- port: 8080
191
192
  ```
192
193
 
193
194
  ## Service: заборонені анотації GKE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.104",
3
+ "version": "1.8.106",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -27,6 +27,22 @@
27
27
  "minLength": 1
28
28
  }
29
29
  },
30
+ "disable-rules": {
31
+ "type": "array",
32
+ "description": "Список rule id (без префікса n-), які не потрібно автододавати в `.n-cursor.json` під час запуску CLI.",
33
+ "items": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ }
37
+ },
38
+ "disable-skills": {
39
+ "type": "array",
40
+ "description": "Список skill id (без префікса n-), які не потрібно автододавати в `.n-cursor.json` під час запуску CLI.",
41
+ "items": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ }
45
+ },
30
46
  "version": {
31
47
  "type": "string",
32
48
  "description": "Застаріле поле, ігнорується CLI. Правила завжди копіюються з каталогу mdc/ установленого пакету (node_modules або кеш npx); змініть версію через оновлення залежності."
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Автовизначення правил і skills для `.n-cursor.json` за умовами з `npm/bin/auto-rules.md`.
3
+ *
4
+ * Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source, кореневий
5
+ * `package.json`) та повертає ідентифікатори правил і skills, які потрібно автододати.
6
+ *
7
+ * Також враховує винятки `disable-rules` і `disable-skills`: елементи з цих списків не
8
+ * додаються автоматично.
9
+ */
10
+ import { existsSync } from 'node:fs'
11
+ import { readdir, readFile } from 'node:fs/promises'
12
+ import { basename, join, relative } from 'node:path'
13
+
14
+ import {
15
+ isGqlScanSourceFile,
16
+ shouldSkipFileForGqlScan,
17
+ sourceFileHasGqlTaggedTemplate
18
+ } from './utils/graphql-gql-scan.mjs'
19
+
20
+ /** Порядок автододавання правил відповідно до `auto-rules.md`. */
21
+ export const AUTO_RULE_ORDER = Object.freeze([
22
+ 'abie',
23
+ 'bun',
24
+ 'docker',
25
+ 'ga',
26
+ 'graphql',
27
+ 'js-lint',
28
+ 'js-pino',
29
+ 'k8s',
30
+ 'nginx-default-tpl',
31
+ 'npm-module',
32
+ 'style-lint',
33
+ 'text',
34
+ 'vue'
35
+ ])
36
+
37
+ /** Порядок автододавання skills відповідно до `auto-rules.md`. */
38
+ export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
39
+
40
+ const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
41
+ const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/iu
42
+ const STYLE_RE = /\.(?:css|vue)$/iu
43
+ const VUE_RE = /\.vue$/iu
44
+ const NGINX_DEFAULT_FILES = new Set(['default.conf.template', 'default.conf', 'nginx.conf'])
45
+ const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
46
+ const DEFAULT_DISABLED_LIST = Object.freeze([])
47
+
48
+ /**
49
+ * Фіксує ознаки, що залежать лише від імені підкаталогу.
50
+ * @param {string} dirName імʼя каталогу
51
+ * @param {{
52
+ * hasK8sDir: boolean,
53
+ * hasTempoDir: boolean
54
+ * }} facts агреговані факти
55
+ * @returns {void}
56
+ */
57
+ function updateDirFacts(dirName, facts) {
58
+ if (dirName === 'k8s') {
59
+ facts.hasK8sDir = true
60
+ }
61
+ if (dirName === 'tempo') {
62
+ facts.hasTempoDir = true
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Фіксує ознаки, що визначаються за шляхом/іменем файлу.
68
+ * @param {string} fileName базове імʼя файлу
69
+ * @param {string} relPath шлях відносно кореня
70
+ * @param {{
71
+ * hasDockerfile: boolean,
72
+ * hasJsLikeSource: boolean,
73
+ * hasNginxDefaultTplFile: boolean,
74
+ * hasVueOrCssSource: boolean,
75
+ * hasVueSource: boolean
76
+ * }} facts агреговані факти
77
+ * @returns {void}
78
+ */
79
+ function updateFileFacts(fileName, relPath, facts) {
80
+ if (fileName === 'Dockerfile' || fileName.startsWith('Dockerfile.')) {
81
+ facts.hasDockerfile = true
82
+ }
83
+ if (NGINX_DEFAULT_FILES.has(fileName)) {
84
+ facts.hasNginxDefaultTplFile = true
85
+ }
86
+ if (JS_LIKE_RE.test(relPath)) {
87
+ facts.hasJsLikeSource = true
88
+ }
89
+ if (VUE_RE.test(relPath)) {
90
+ facts.hasVueSource = true
91
+ }
92
+ if (STYLE_RE.test(relPath)) {
93
+ facts.hasVueOrCssSource = true
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Чи потрібно сканувати файл на gql tagged template.
99
+ * @param {string} relPath шлях відносно кореня
100
+ * @param {{ hasGqlTaggedTemplates: boolean }} facts агреговані факти
101
+ * @returns {boolean} true, якщо файл варто сканувати
102
+ */
103
+ function shouldScanFileForGql(relPath, facts) {
104
+ return !facts.hasGqlTaggedTemplates && isGqlScanSourceFile(relPath) && !shouldSkipFileForGqlScan(relPath)
105
+ }
106
+
107
+ /**
108
+ * Оновлює ознаку `hasGqlTaggedTemplates` за вмістом конкретного файлу.
109
+ * @param {string} absPath абсолютний шлях до файлу
110
+ * @param {string} relPath шлях відносно кореня
111
+ * @param {{ hasGqlTaggedTemplates: boolean }} facts агреговані факти
112
+ * @returns {Promise<void>}
113
+ */
114
+ async function updateGqlFactFromFile(absPath, relPath, facts) {
115
+ try {
116
+ const content = await readFile(absPath, 'utf8')
117
+ if (sourceFileHasGqlTaggedTemplate(content, relPath)) {
118
+ facts.hasGqlTaggedTemplates = true
119
+ }
120
+ } catch {
121
+ /* ігноруємо пошкоджені/недоступні файли */
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Обробляє файл під час обходу дерева.
127
+ * @param {string} absPath абсолютний шлях до файлу
128
+ * @param {string} root абсолютний шлях кореня
129
+ * @param {{
130
+ * hasDockerfile: boolean,
131
+ * hasGqlTaggedTemplates: boolean,
132
+ * hasJsLikeSource: boolean,
133
+ * hasNginxDefaultTplFile: boolean,
134
+ * hasVueOrCssSource: boolean,
135
+ * hasVueSource: boolean
136
+ * }} facts агреговані факти
137
+ * @returns {Promise<void>}
138
+ */
139
+ async function processFileEntry(absPath, root, facts) {
140
+ const rel = relative(root, absPath).split('\\').join('/')
141
+ const fileName = basename(absPath)
142
+ updateFileFacts(fileName, rel, facts)
143
+ if (shouldScanFileForGql(rel, facts)) {
144
+ await updateGqlFactFromFile(absPath, rel, facts)
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Нормалізує список ідентифікаторів (trim + lowercase + унікальність збереженням порядку).
150
+ * @param {unknown} value вихідне значення з `.n-cursor.json`
151
+ * @returns {string[]} масив id у нормалізованому вигляді
152
+ */
153
+ export function normalizeIdList(value) {
154
+ if (!Array.isArray(value)) {
155
+ return []
156
+ }
157
+ const out = []
158
+ for (const item of value) {
159
+ const normalized = String(item).trim().toLowerCase()
160
+ if (normalized && !out.includes(normalized)) {
161
+ out.push(normalized)
162
+ }
163
+ }
164
+ return out
165
+ }
166
+
167
+ /**
168
+ * Повертає URL репозиторію з package.json (`repository` може бути рядком або обʼєктом).
169
+ * @param {unknown} repository значення `packageJson.repository`
170
+ * @returns {string | null} URL або null
171
+ */
172
+ export function getRepositoryUrl(repository) {
173
+ if (typeof repository === 'string') {
174
+ return repository
175
+ }
176
+ if (repository && typeof repository === 'object' && !Array.isArray(repository)) {
177
+ const url = /** @type {Record<string, unknown>} */ (repository).url
178
+ if (typeof url === 'string') {
179
+ return url
180
+ }
181
+ }
182
+ return null
183
+ }
184
+
185
+ /**
186
+ * Чи package.json виглядає як монорепо (поле `workspaces`).
187
+ * @param {unknown} packageJson кореневий package.json як JS-обʼєкт
188
+ * @returns {boolean} true, якщо оголошено workspaces
189
+ */
190
+ export function isMonorepoPackage(packageJson) {
191
+ if (packageJson === null || typeof packageJson !== 'object' || Array.isArray(packageJson)) {
192
+ return false
193
+ }
194
+ const workspaces = /** @type {Record<string, unknown>} */ (packageJson).workspaces
195
+ if (Array.isArray(workspaces)) {
196
+ return workspaces.length > 0
197
+ }
198
+ if (workspaces && typeof workspaces === 'object' && !Array.isArray(workspaces)) {
199
+ const packages = /** @type {Record<string, unknown>} */ (workspaces).packages
200
+ return Array.isArray(packages) && packages.length > 0
201
+ }
202
+ return false
203
+ }
204
+
205
+ /**
206
+ * Обходить дерево проєкту, збираючи факти для автоувімкнення правил.
207
+ * @param {string} root абсолютний шлях кореня репозиторію
208
+ * @returns {Promise<{
209
+ * hasDockerfile: boolean,
210
+ * hasGaWorkflowsDir: boolean,
211
+ * hasGqlTaggedTemplates: boolean,
212
+ * hasJsLikeSource: boolean,
213
+ * hasK8sDir: boolean,
214
+ * hasNginxDefaultTplFile: boolean,
215
+ * hasTempoDir: boolean,
216
+ * hasVueSource: boolean,
217
+ * hasVueOrCssSource: boolean
218
+ * }>} агреговані факти
219
+ */
220
+ export async function collectAutoRuleFacts(root) {
221
+ const facts = {
222
+ hasDockerfile: false,
223
+ hasGaWorkflowsDir: existsSync(join(root, '.github', 'workflows')),
224
+ hasGqlTaggedTemplates: false,
225
+ hasJsLikeSource: false,
226
+ hasK8sDir: false,
227
+ hasNginxDefaultTplFile: false,
228
+ hasTempoDir: false,
229
+ hasVueSource: false,
230
+ hasVueOrCssSource: false
231
+ }
232
+
233
+ /**
234
+ * Рекурсивний обхід каталогу з пропуском службових директорій.
235
+ * @param {string} dir абсолютний шлях каталогу
236
+ * @returns {Promise<void>}
237
+ */
238
+ async function walk(dir) {
239
+ let entries
240
+ try {
241
+ entries = await readdir(dir, { withFileTypes: true })
242
+ } catch {
243
+ return
244
+ }
245
+
246
+ for (const entry of entries) {
247
+ const absPath = join(dir, entry.name)
248
+ if (entry.isDirectory()) {
249
+ const isIgnoredDir = IGNORED_DIR_NAMES.has(entry.name)
250
+ if (!isIgnoredDir) {
251
+ updateDirFacts(entry.name, facts)
252
+ await walk(absPath)
253
+ }
254
+ } else if (entry.isFile()) {
255
+ await processFileEntry(absPath, root, facts)
256
+ }
257
+ }
258
+ }
259
+
260
+ await walk(root)
261
+ return facts
262
+ }
263
+
264
+ /**
265
+ * Визначає авто-правила та skills згідно з `auto-rules.md`.
266
+ * @param {object} params параметри аналізу
267
+ * @param {string} params.root абсолютний шлях до кореня репозиторію
268
+ * @param {string[]} params.availableRules перелік доступних правил з пакету
269
+ * @param {string[]} params.availableSkills перелік доступних skills з пакету
270
+ * @param {unknown} params.packageJsonParsed кореневий package.json (розпарсений) або null
271
+ * @param {string[]} [params.disableRules] список `disable-rules` з конфігу
272
+ * @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
273
+ * @returns {Promise<{ rules: string[], skills: string[] }>} списки id у стабільному порядку
274
+ */
275
+ export async function detectAutoRulesAndSkills({
276
+ root,
277
+ availableRules,
278
+ availableSkills,
279
+ packageJsonParsed,
280
+ disableRules = DEFAULT_DISABLED_LIST,
281
+ disableSkills = DEFAULT_DISABLED_LIST
282
+ }) {
283
+ const facts = await collectAutoRuleFacts(root)
284
+ const normalizedRules = new Set(availableRules.map(r => r.trim().toLowerCase()))
285
+ const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
286
+ const disableRulesSet = new Set(disableRules)
287
+ const disableSkillsSet = new Set(disableSkills)
288
+
289
+ const packageJsonExists = existsSync(join(root, 'package.json'))
290
+ const npmDirExists = existsSync(join(root, 'npm'))
291
+ const repositoryUrl = getRepositoryUrl(
292
+ packageJsonParsed && typeof packageJsonParsed === 'object' && !Array.isArray(packageJsonParsed)
293
+ ? /** @type {Record<string, unknown>} */ (packageJsonParsed).repository
294
+ : null
295
+ )
296
+ const isAbie = typeof repositoryUrl === 'string' && repositoryUrl.toLowerCase().includes(ABIE_REPOSITORY_URL_MARKER)
297
+ const isMonorepo = isMonorepoPackage(packageJsonParsed)
298
+
299
+ /** @type {string[]} */
300
+ const detectedRules = []
301
+ /** @type {string[]} */
302
+ const detectedSkills = []
303
+
304
+ /**
305
+ * Додає правило до результату, якщо воно доступне і не в disable-списку.
306
+ * @param {string} ruleId id правила
307
+ * @returns {void}
308
+ */
309
+ function addRule(ruleId) {
310
+ if (!normalizedRules.has(ruleId) || disableRulesSet.has(ruleId) || detectedRules.includes(ruleId)) {
311
+ return
312
+ }
313
+ detectedRules.push(ruleId)
314
+ }
315
+
316
+ /**
317
+ * Додає skill до результату, якщо він доступний і не в disable-списку.
318
+ * @param {string} skillId id skill
319
+ * @returns {void}
320
+ */
321
+ function addSkill(skillId) {
322
+ if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detectedSkills.includes(skillId)) {
323
+ return
324
+ }
325
+ detectedSkills.push(skillId)
326
+ }
327
+
328
+ const autoRuleChecks = [
329
+ { enabled: isAbie, id: 'abie' },
330
+ { enabled: packageJsonExists, id: 'bun' },
331
+ { enabled: facts.hasDockerfile, id: 'docker' },
332
+ { enabled: facts.hasGaWorkflowsDir, id: 'ga' },
333
+ { enabled: facts.hasGqlTaggedTemplates, id: 'graphql' },
334
+ { enabled: facts.hasJsLikeSource, id: 'js-lint' },
335
+ { enabled: facts.hasJsLikeSource && !(isMonorepo && facts.hasVueSource && facts.hasTempoDir), id: 'js-pino' },
336
+ { enabled: facts.hasK8sDir, id: 'k8s' },
337
+ { enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
338
+ { enabled: npmDirExists, id: 'npm-module' },
339
+ { enabled: facts.hasVueOrCssSource, id: 'style-lint' }
340
+ ]
341
+ for (const item of autoRuleChecks) {
342
+ if (item.enabled) {
343
+ addRule(item.id)
344
+ }
345
+ }
346
+ addRule('text')
347
+ if (facts.hasVueSource) {
348
+ addRule('vue')
349
+ }
350
+
351
+ const autoSkillChecks = [
352
+ { enabled: isAbie, id: 'abie-kustomize' },
353
+ { enabled: true, id: 'fix' },
354
+ { enabled: true, id: 'lint' }
355
+ ]
356
+ for (const item of autoSkillChecks) {
357
+ if (item.enabled) {
358
+ addSkill(item.id)
359
+ }
360
+ }
361
+
362
+ const rules = AUTO_RULE_ORDER.filter(ruleId => detectedRules.includes(ruleId))
363
+ const skills = AUTO_SKILL_ORDER.filter(skillId => detectedSkills.includes(skillId))
364
+ return { rules, skills }
365
+ }
366
+
367
+ /**
368
+ * Доповнює конфіг автодетектом (лише додає; існуючі вручну задані елементи не прибирає).
369
+ * @param {object} params параметри оновлення
370
+ * @param {{ rules: unknown, skills?: unknown, ['disable-rules']?: unknown, ['disable-skills']?: unknown }} params.config розпарсений `.n-cursor.json`
371
+ * @param {string[]} params.detectedRules правила, визначені автодетектом
372
+ * @param {string[]} params.detectedSkills skills, визначені автодетектом
373
+ * @returns {{ rules: string[], skills: string[] } & Record<string, unknown>} новий нормалізований конфіг
374
+ */
375
+ export function mergeConfigWithAutoDetected({ config, detectedRules, detectedSkills }) {
376
+ const existingRules = normalizeIdList(config.rules)
377
+ const existingSkills = normalizeIdList(config.skills)
378
+ const disableRules = normalizeIdList(config['disable-rules'])
379
+ const disableSkills = normalizeIdList(config['disable-skills'])
380
+
381
+ const rules = [...existingRules]
382
+ for (const id of detectedRules) {
383
+ if (!rules.includes(id) && !disableRules.includes(id)) {
384
+ rules.push(id)
385
+ }
386
+ }
387
+
388
+ const skills = [...existingSkills]
389
+ for (const id of detectedSkills) {
390
+ if (!skills.includes(id) && !disableSkills.includes(id)) {
391
+ skills.push(id)
392
+ }
393
+ }
394
+
395
+ /** @type {{ rules: string[], skills: string[] } & Record<string, unknown>} */
396
+ const normalized = { rules, skills }
397
+ if (disableRules.length > 0) {
398
+ normalized['disable-rules'] = disableRules
399
+ }
400
+ if (disableSkills.length > 0) {
401
+ normalized['disable-skills'] = disableSkills
402
+ }
403
+ return normalized
404
+ }