@nitra/cursor 1.13.48 → 1.13.50

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.50] - 2026-05-19
8
+
9
+ ### Changed
10
+
11
+ - `lint-k8s`: kubescape тепер збирає kustomize-маніфест через **вшиту в kubectl підкоманду** — `kubectl kustomize <dir> | kubescape scan -` (замість окремого бінарника `kustomize build <dir>`, доданого в 1.13.49). Причина: на машинах без окремого `kustomize` lint-k8s падав з `kustomize не знайдено в PATH`, тоді як `kubectl` — штатний інструмент з вшитим Kustomize (рендеринг локальний, доступ до кластера не потрібен). PATH-залежність зведена з пари `kubectl+kustomize` до одного `kubectl`; крок `Install kustomize` у GHA-шаблоні `lint-k8s.yml` прибрано (на github-hosted runner'ах kubectl уже доступний). Bump `k8s.mdc` `1.37` → `1.38`.
12
+
13
+ ## [1.13.49] - 2026-05-19
14
+
15
+ ### Changed
16
+
17
+ - `lint-k8s`: kubescape тепер сканує **зібраний kustomize-маніфест** через stdin (`kustomize build <dir> | kubescape scan -`) для кожного dir-у з `kustomization.yaml` під `…/k8s` (Kustomize Components — `kind: Component` — пропускаються, вони не білдяться окремо). Це усуває false-positive **C-0260** (`Missing network policy`) у каноні з sibling `components/networkpolicy.yaml` без `metadata.namespace`: сирий dir-скан не виконував kustomize, бачив порожній namespace у NetworkPolicy проти непорожнього у Deployment з `base/`, через що `podSelector` не матчився. Якщо `kustomization.yaml` під коренем `…/k8s` немає — fallback на старий dir-скан. Нова PATH-залежність — `kustomize` (додано крок у GHA-шаблоні `lint-k8s.yml`). Bump `k8s.mdc` `1.36` → `1.37`.
18
+
7
19
  ## [1.13.48] - 2026-05-19
8
20
 
9
21
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.48",
3
+ "version": "1.13.50",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -40,8 +40,10 @@ all_run_text := concat("\n", [run_text |
40
40
  run_text := step_run_to_text(step)
41
41
  ])
42
42
 
43
+ # conftest парсить YAML 1.1, тож канонічний `on:` без лапок стає булевим ключем
44
+ # `true` (як у `ga.lint_ga`). Тому читаємо через `input["true"]`.
43
45
  push_paths_set := {p |
44
- some p in object.get(object.get(object.get(input, "on", {}), "push", {}), "paths", [])
46
+ some p in object.get(object.get(object.get(input, "true", {}), "push", {}), "paths", [])
45
47
  }
46
48
 
47
49
  # ── deny: on.push.paths subset-of ──────────────────────────────────────
package/rules/k8s/k8s.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
3
- version: '1.36'
3
+ version: '1.38'
4
4
  globs: "**/k8s/**/*.yaml"
5
5
  alwaysApply: false
6
6
  ---
@@ -29,11 +29,11 @@ alwaysApply: false
29
29
 
30
30
  Окремо від modeline `$schema` у редакторі варто ганяти CLI-лінтери (**kubeconform** і **kubescape**) по тих самих дерев’ях **`…/k8s`**.
31
31
 
32
- **Залежності:** виконувані файли kubeconform і kubescape у **PATH**; не додавай їх у **devDependencies**.
32
+ **Залежності:** виконувані файли kubeconform, kubescape і kubectl у **PATH** (kustomize використовуємо як вшиту підкоманду **`kubectl kustomize`** — окремий бінарник `kustomize` не потрібен); не додавай їх у **devDependencies**.
33
33
 
34
34
  **Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`check-k8s.mjs`** (зараз **`-kubernetes-version 1.33.9`** — semver без префікса `v`, еквівалент релізу **v1.33.9**; набір схем **`v1.33.9-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
35
35
 
36
- **kubescape:** типово **`kubescape scan <каталог-k8s>`**; поріг серйозності підлаштуй під проєкт (наприклад **`--severity-threshold high`**). Перший запуск може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
36
+ **kubescape вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir> | kubescape scan -`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Це усуває false-positive **C-0260** (`Missing network policy`) у каноні з sibling **`components/networkpolicy.yaml`** без `metadata.namespace`: сирий dir-скан не виконує kustomize-збірку й бачить порожній namespace у NetworkPolicy проти непорожнього у Deployment з `base/`, тож `podSelector` не матчиться. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
37
37
 
38
38
  ### Винятки kubescape: `.kubescape-exceptions.json`
39
39
 
@@ -99,6 +99,9 @@ jobs:
99
99
  curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
100
100
  echo "$HOME/.kubescape/bin" >> $GITHUB_PATH
101
101
 
102
+ # kustomize не встановлюємо окремо — використовуємо вбудовану підкоманду `kubectl kustomize`
103
+ # (kubectl уже доступний на github-hosted runner'ах: https://github.com/actions/runner-images).
104
+
102
105
  - name: Lint K8s
103
106
  run: bun run lint-k8s
104
107
  ```
@@ -14,8 +14,11 @@
14
14
  */
15
15
  import { spawnSync } from 'node:child_process'
16
16
  import { existsSync } from 'node:fs'
17
+ import { readFile } from 'node:fs/promises'
17
18
  import { basename, dirname, join, relative } from 'node:path'
18
19
 
20
+ import { parse } from 'yaml'
21
+
19
22
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
20
23
  import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
21
24
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
@@ -24,6 +27,9 @@ import { walkDir } from '../../../scripts/utils/walkDir.mjs'
24
27
  /** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
25
28
  const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
26
29
 
30
+ /** Назва kustomization-файлу (під `k8s` дозволено лише `.yaml`, див. k8s.mdc). */
31
+ const KUSTOMIZATION_FILE = 'kustomization.yaml'
32
+
27
33
  const PATH_SEPARATOR_RE = /[/\\]/u
28
34
  const YAML_EXT_RE = /\.yaml$/iu
29
35
 
@@ -134,37 +140,138 @@ export function buildKubescapeExceptionsArgs(root) {
134
140
  }
135
141
 
136
142
  /**
137
- * Запускає kubescape scan для кожного каталогу окремо (узгоджено з прикладами CLI).
138
- * Немає прапорця версії Kubernetes за потреби додай `scan framework <ім’я>` під CIS/інші набори.
143
+ * Знаходить каталоги-«точки входу» Kustomize під `dir` ті, що містять `kustomization.yaml`,
144
+ * чий перший YAML-документ має `kind: Kustomization` (або без `kind` типово Kustomization).
145
+ * Kustomize Components (`kind: Component`) пропускаються: вони не білдяться окремо,
146
+ * а підключаються через `components:` із overlay (див. k8s.mdc, секція «Kustomize: структура каталогів»).
139
147
  *
140
- * Якщо в корені проєкту є `.kubescape-exceptions.json` підмішується через `--exceptions <file>`.
141
- * Файл потрібен для точкових винятків control'ів kubescape (напр. C-0012 на ConfigMap, що містить
142
- * публічний JWT-конфіг типу `HASURA_GRAPHQL_JWT_SECRET={"jwk_url": "https://…"}` control тригериться
143
- * на ім'я env, а не на значення; див. приклад у `k8s.mdc`).
148
+ * Семантика збігається з реальною поведінкою `kustomize build <dir>`: воно зчитує саме
149
+ * `kustomization.yaml`, тож пошук іде за назвою файлу (без `.yml`-варіанту заборонено каноном).
150
+ * @param {string} dir абсолютний шлях до `…/k8s`
151
+ * @returns {Promise<string[]>} відсортовані абсолютні шляхи до dir-ів з білдабельним `kustomization.yaml`
152
+ */
153
+ export async function findKustomizationDirs(dir) {
154
+ /** @type {string[]} */
155
+ const candidates = []
156
+ await walkDir(dir, p => {
157
+ if (basename(p) === KUSTOMIZATION_FILE) candidates.push(p)
158
+ })
159
+ /** @type {Set<string>} */
160
+ const result = new Set()
161
+ for (const p of candidates) {
162
+ let text
163
+ try {
164
+ text = await readFile(p, 'utf8')
165
+ } catch {
166
+ continue
167
+ }
168
+ let doc
169
+ try {
170
+ doc = parse(text)
171
+ } catch {
172
+ continue
173
+ }
174
+ if (doc && typeof doc === 'object' && doc.kind === 'Component') continue
175
+ result.add(dirname(p))
176
+ }
177
+ return [...result].toSorted((a, b) => a.localeCompare(b))
178
+ }
179
+
180
+ /**
181
+ * Запускає `kubectl kustomize <dir>` і повертає stdout як буфер. stderr інхеритимо в термінал,
182
+ * щоб помилки збірки (биті посилання, недозволені плагіни) було видно одразу. Використовуємо
183
+ * вшитий у kubectl kustomize замість окремого бінарника — kubectl є штатним інструментом і не
184
+ * потребує доступу до кластера для підкоманди `kustomize` (рендеринг локальний).
185
+ * @param {string} kubectlPath абсолютний шлях до бінарника kubectl
186
+ * @param {string} dir абсолютний шлях до каталогу з `kustomization.yaml`
187
+ * @returns {{ status: number, stdout: Buffer }} статус процесу і зібраний маніфест
188
+ */
189
+ function runKustomizeBuild(kubectlPath, dir) {
190
+ const r = spawnSync(kubectlPath, ['kustomize', dir], {
191
+ stdio: ['ignore', 'pipe', 'inherit'],
192
+ shell: false
193
+ })
194
+ return { status: r.status ?? 1, stdout: r.stdout ?? Buffer.alloc(0) }
195
+ }
196
+
197
+ /**
198
+ * Запускає `kubescape scan -` зі stdin (буфер `manifest`). stdout/stderr інхерит у термінал.
199
+ * @param {string} kubescapePath абсолютний шлях до бінарника kubescape
200
+ * @param {Buffer} manifest зібраний kustomize-маніфест
201
+ * @param {string[]} exceptionsArgs `['--exceptions', '<file>']` або `[]`
202
+ * @returns {{ status: number, enoent: boolean }} статус процесу і прапор ENOENT
203
+ */
204
+ function runKubescapeStdin(kubescapePath, manifest, exceptionsArgs) {
205
+ const r = spawnSync(kubescapePath, ['scan', '-', '--severity-threshold', 'high', ...exceptionsArgs], {
206
+ input: manifest,
207
+ stdio: ['pipe', 'inherit', 'inherit'],
208
+ shell: false
209
+ })
210
+ const enoent = Boolean(r.error && 'code' in r.error && r.error.code === 'ENOENT')
211
+ return { status: r.status ?? 1, enoent }
212
+ }
213
+
214
+ /**
215
+ * Запускає kubescape по зібраному kustomize-маніфесту для кожного `…/k8s`-кореня. Для кожного
216
+ * dir-у з `kustomization.yaml` (крім `kind: Component`) робимо `kubectl kustomize <dir>` і піпимо
217
+ * stdout у `kubescape scan -`. Це усуває false-positive C-0260 (`Missing network policy`) у випадках,
218
+ * коли NetworkPolicy живе у sibling `components/` без `metadata.namespace` (намспейс інжектить
219
+ * overlay через `kustomization.namespace`); сирий dir-скан не виконує kustomize й бачить порожній
220
+ * `namespace` у NetworkPolicy проти непорожнього у Deployment, через що `podSelector` не матчиться.
221
+ *
222
+ * Якщо в `…/k8s`-корені немає жодного білдабельного kustomization.yaml (проєкт без Kustomize) —
223
+ * fallback на старий dir-скан, щоб не блокувати чистий YAML-only набір маніфестів.
224
+ *
225
+ * Якщо в корені репо є `.kubescape-exceptions.json` — підмішується через `--exceptions <file>`
226
+ * (точкові винятки control'ів, напр. C-0012 на ConfigMap з публічним JWT-конфігом; див. k8s.mdc).
144
227
  * @param {string[]} dirs абсолютні шляхи до `…/k8s`
145
228
  * @param {string} root корінь репозиторію (для пошуку exceptions-файлу)
146
- * @returns {number} 0 при успіху, інакше код останнього невдалого scan або 127, якщо kubescape відсутній у PATH
229
+ * @returns {Promise<number>} 0 при успіху, інакше код невдалого процесу або 127, якщо kubescape/kubectl відсутні
147
230
  */
148
- function runKubescape(dirs, root) {
231
+ async function runKubescape(dirs, root) {
149
232
  const exceptionsArgs = buildKubescapeExceptionsArgs(root)
150
233
  if (exceptionsArgs.length > 0) {
151
234
  console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
152
235
  }
236
+ const kubescapePath = resolveCmd('kubescape')
237
+ if (!kubescapePath) {
238
+ console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
239
+ return 127
240
+ }
241
+ let kubectlPath = null
153
242
  for (const d of dirs) {
154
- const kubescapePath = resolveCmd('kubescape')
155
- if (!kubescapePath) {
156
- console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
157
- return 127
243
+ const kdirs = await findKustomizationDirs(d)
244
+ if (kdirs.length === 0) {
245
+ console.log(`run-k8s: kubescape scan ${d} (без kustomization сирий dir-скан)`)
246
+ const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high', ...exceptionsArgs], {
247
+ stdio: 'inherit',
248
+ shell: false
249
+ })
250
+ if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
251
+ console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
252
+ return 127
253
+ }
254
+ if (r.status !== 0) return r.status ?? 1
255
+ continue
256
+ }
257
+ if (kubectlPath === null) {
258
+ kubectlPath = resolveCmd('kubectl')
259
+ if (!kubectlPath) {
260
+ console.error('kubectl не знайдено в PATH. Встанови з https://kubernetes.io/docs/tasks/tools/#kubectl')
261
+ return 127
262
+ }
158
263
  }
159
- const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high', ...exceptionsArgs], {
160
- stdio: 'inherit',
161
- shell: false
162
- })
163
- if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
164
- console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
165
- return 127
264
+ for (const kdir of kdirs) {
265
+ console.log(`run-k8s: kubectl kustomize ${kdir} | kubescape scan -`)
266
+ const build = runKustomizeBuild(kubectlPath, kdir)
267
+ if (build.status !== 0) return build.status
268
+ const ks = runKubescapeStdin(kubescapePath, build.stdout, exceptionsArgs)
269
+ if (ks.enoent) {
270
+ console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
271
+ return 127
272
+ }
273
+ if (ks.status !== 0) return ks.status
166
274
  }
167
- if (r.status !== 0) return r.status ?? 1
168
275
  }
169
276
  return 0
170
277
  }
@@ -190,7 +297,7 @@ export async function runLintK8s() {
190
297
  const kc = runKubeconform(dirs)
191
298
  if (kc !== 0) return kc
192
299
 
193
- const ks = runKubescape(dirs, root)
300
+ const ks = await runKubescape(dirs, root)
194
301
  return ks
195
302
  }
196
303
 
@@ -19,6 +19,10 @@ expected_runs_on := data.template.snippet.jobs.text["runs-on"]
19
19
 
20
20
  expected_perms := data.template.snippet.jobs.text.permissions
21
21
 
22
+ # conftest парсить YAML 1.1, де канонічний `on:` без лапок стає булевим ключем
23
+ # `true` (як у `ga.lint_ga`). Тому читаємо on-блок через `input["true"]`.
24
+ gha_on := input["true"]
25
+
22
26
  job := input.jobs.text
23
27
 
24
28
  job_uses_set contains job.steps[_].uses
@@ -45,17 +49,17 @@ deny contains msg if {
45
49
  }
46
50
 
47
51
  deny contains msg if {
48
- not branches_superset_of(input.on.push.branches, expected_push_branches)
52
+ not branches_superset_of(gha_on.push.branches, expected_push_branches)
49
53
  msg := "lint-text.yml: on.push.branches має містити dev і main (text.mdc)"
50
54
  }
51
55
 
52
56
  deny contains msg if {
53
- not branches_superset_of(input.on.pull_request.branches, expected_pr_branches)
57
+ not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
54
58
  msg := "lint-text.yml: on.pull_request.branches має містити dev і main (text.mdc)"
55
59
  }
56
60
 
57
61
  deny contains msg if {
58
- not paths_superset_of(input.on.push.paths, expected_push_paths)
62
+ not paths_superset_of(gha_on.push.paths, expected_push_paths)
59
63
  msg := "lint-text.yml: on.push.paths має містити очікувані glob-и (text.mdc)"
60
64
  }
61
65