@nitra/cursor 1.8.111 → 1.8.112

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/abie.mdc CHANGED
@@ -116,7 +116,7 @@ spec:
116
116
  ## k8s: overlay **ru** і nginx-sidecar для WebSocket (Hasura)
117
117
 
118
118
  YC ALB (gwin) має баг: якщо HTTPRoute-правило містить одночасно `URLRewrite` (ReplacePrefixMatch) і `upgrade_types: websocket` — ALB не обробляє WebSocket і повертає 404.
119
- https://center.yandex.cloud/support/tickets/TX549394
119
+ <https://center.yandex.cloud/support/tickets/TX549394>
120
120
 
121
121
  Обхідний варіант: nginx-sidecar у поді, що сам виконує rewrite і проксіює WebSocket до Hasura.
122
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.111",
3
+ "version": "1.8.112",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -89,6 +89,23 @@ const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}
89
89
  const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
90
90
  const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
91
91
 
92
+ /** Підрядок образу Hasura у контейнері Deployment (abie.mdc nginx-sidecar). */
93
+ const HASURA_IMAGE_MARKER = 'hasura/graphql-engine'
94
+ /** Nginx-sidecar image (abie.mdc): nginx:*-alpine. */
95
+ const NGINX_SIDECAR_IMAGE_RE = /image:\s*nginx:\S*-alpine/u
96
+ /** containerPort: 8081 у patch Deployment (abie.mdc). */
97
+ const NGINX_SIDECAR_CONTAINER_PORT_RE = /containerPort:\s*8081\b/u
98
+ /** port: 8081 у patch Service -hl (proxy порт, abie.mdc). */
99
+ const PATCH_PROXY_PORT_8081_RE = /\bport:\s*8081\b/u
100
+ /** configmap-nginx.yaml у resources kustomization (abie.mdc). */
101
+ const RESOURCES_CONFIGMAP_NGINX_RE = /configmap-nginx\.yaml/u
102
+ /** path /spec/rules/{i}/backendRefs/{j}/port … value: 8081 у patch HTTPRoute (path→value, abie.mdc). */
103
+ const HTTPROUTE_BACKENDREF_PORT_8081_RE =
104
+ /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b[\s\S]{0,200}?value:\s*8081\b/mu
105
+ /** Те саме, value→path. */
106
+ const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
107
+ /value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
108
+
92
109
  /** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
93
110
  export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
94
111
 
@@ -1627,6 +1644,203 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
1627
1644
  }
1628
1645
  }
1629
1646
 
1647
+ /**
1648
+ * Чи Deployment-документ містить контейнер із образом **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
1649
+ * @param {unknown} obj корінь YAML-документа
1650
+ * @returns {boolean}
1651
+ */
1652
+ function deploymentDocHasHasuraImage(obj) {
1653
+ if (!isDeploymentDoc(obj)) return false
1654
+ const rec = /** @type {Record<string, unknown>} */ (obj)
1655
+ const spec = rec.spec
1656
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
1657
+ const template = /** @type {Record<string, unknown>} */ (spec).template
1658
+ if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
1659
+ const podSpec = /** @type {Record<string, unknown>} */ (template).spec
1660
+ if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
1661
+ const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
1662
+ if (!Array.isArray(containers)) return false
1663
+ for (const c of containers) {
1664
+ if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
1665
+ const img = /** @type {Record<string, unknown>} */ (c).image
1666
+ if (typeof img === 'string' && img.includes(HASURA_IMAGE_MARKER)) return true
1667
+ }
1668
+ }
1669
+ return false
1670
+ }
1671
+
1672
+ /**
1673
+ * Чи Kustomization-документ містить у **`images[*].newName`** рядок **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
1674
+ * @param {unknown} obj корінь YAML-документа
1675
+ * @returns {boolean}
1676
+ */
1677
+ function kustomizationDocHasHasuraImageNewName(obj) {
1678
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return false
1679
+ const rec = /** @type {Record<string, unknown>} */ (obj)
1680
+ if (!Array.isArray(rec.images)) return false
1681
+ for (const img of rec.images) {
1682
+ if (img !== null && typeof img === 'object' && !Array.isArray(img)) {
1683
+ const newName = /** @type {Record<string, unknown>} */ (img).newName
1684
+ if (typeof newName === 'string' && newName.includes(HASURA_IMAGE_MARKER)) return true
1685
+ }
1686
+ }
1687
+ return false
1688
+ }
1689
+
1690
+ /**
1691
+ * Збирає тексти inline **patch** для **Deployment** з **kustomization.yaml** (усі документи).
1692
+ * @param {string} raw повний текст файлу
1693
+ * @returns {string[]} рядки patch
1694
+ */
1695
+ function collectDeploymentPatchTextsFromKustomization(raw) {
1696
+ const body = stripBom(raw)
1697
+ const lines = body.split(LINE_SPLIT_RE)
1698
+ const first = lines[0] ?? ''
1699
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
1700
+ /** @type {import('yaml').Document[]} */
1701
+ let docs
1702
+ try {
1703
+ docs = parseAllDocuments(rest)
1704
+ } catch {
1705
+ return []
1706
+ }
1707
+ /** @type {string[]} */
1708
+ const out = []
1709
+ for (const doc of docs) {
1710
+ if (doc.errors.length > 0) continue
1711
+ const root = doc.toJSON()
1712
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) continue
1713
+ const rec = /** @type {Record<string, unknown>} */ (root)
1714
+ if (!Array.isArray(rec.patches)) continue
1715
+ for (const p of rec.patches) {
1716
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
1717
+ const pr = /** @type {Record<string, unknown>} */ (p)
1718
+ const target = pr.target
1719
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) continue
1720
+ const tg = /** @type {Record<string, unknown>} */ (target)
1721
+ if (tg.kind !== 'Deployment') continue
1722
+ const patchStr = pr.patch
1723
+ if (typeof patchStr === 'string' && patchStr.trim() !== '') out.push(patchStr)
1724
+ }
1725
+ }
1726
+ return out
1727
+ }
1728
+
1729
+ /**
1730
+ * Каталоги пакетів, де в дереві **k8s** є **Deployment** з образом **`hasura/graphql-engine`** або
1731
+ * **Kustomization** з **`images[*].newName`** на нього (abie.mdc nginx-sidecar).
1732
+ * @param {string} root корінь репозиторію
1733
+ * @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
1734
+ * @returns {Promise<Set<string>>} абсолютні шляхи каталогів пакетів
1735
+ */
1736
+ async function collectHasuraK8sPackageDirs(root, yamlAbs) {
1737
+ /** @type {Set<string>} */
1738
+ const dirs = new Set()
1739
+ for (const abs of yamlAbs) {
1740
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
1741
+ const docs = await readAndParseYamlDocs(abs, rel, silentFail)
1742
+ if (!docs) continue
1743
+ for (const doc of docs) {
1744
+ if (doc.errors.length > 0) continue
1745
+ const obj = doc.toJSON()
1746
+ if (deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)) {
1747
+ const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
1748
+ if (pkgDir) dirs.add(pkgDir)
1749
+ }
1750
+ }
1751
+ }
1752
+ return dirs
1753
+ }
1754
+
1755
+ /**
1756
+ * Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
1757
+ * **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
1758
+ * **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
1759
+ * patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
1760
+ * @param {string} root корінь репозиторію
1761
+ * @param {string[]} yamlFilesAbs yaml під k8s
1762
+ * @param {(msg: string) => void} fail callback при помилці
1763
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
1764
+ * @returns {Promise<void>}
1765
+ */
1766
+ async function ensureAbieNginxSidecarForHasura(root, yamlFilesAbs, fail, passFn) {
1767
+ const hasuraPkgDirs = await collectHasuraK8sPackageDirs(root, yamlFilesAbs)
1768
+ if (hasuraPkgDirs.size === 0) {
1769
+ passFn('Немає Deployment із hasura/graphql-engine у дереві k8s — nginx-sidecar не вимагається (abie.mdc)')
1770
+ return
1771
+ }
1772
+ for (const pkgAbs of [...hasuraPkgDirs].toSorted((a, b) => a.localeCompare(b))) {
1773
+ const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
1774
+ const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
1775
+ if (!existsSync(ruAbs)) {
1776
+ passFn(`${relPkg}/k8s: є Hasura Deployment, але немає ru/kustomization.yaml — nginx-sidecar не перевіряється`)
1777
+ continue
1778
+ }
1779
+ let ruRaw
1780
+ try {
1781
+ ruRaw = await readFile(ruAbs, 'utf8')
1782
+ } catch (error) {
1783
+ const msg = error instanceof Error ? error.message : String(error)
1784
+ fail(`${relPkg}/k8s/ru/kustomization.yaml: не вдалося прочитати (${msg})`)
1785
+ return
1786
+ }
1787
+ if (!ruRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)) {
1788
+ passFn(
1789
+ `${relPkg}/k8s/ru/kustomization.yaml: немає ${HASURA_JWT_SECRET_IN_KUSTOMIZATION} — nginx-sidecar не вимагається (abie.mdc)`
1790
+ )
1791
+ continue
1792
+ }
1793
+ const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
1794
+ // configmap-nginx.yaml must exist
1795
+ const configmapNginxAbs = join(pkgAbs, 'k8s', 'ru', 'configmap-nginx.yaml')
1796
+ if (!existsSync(configmapNginxAbs)) {
1797
+ fail(`${relPkg}/k8s/ru: потрібен configmap-nginx.yaml з nginx.conf (nginx-sidecar для Hasura WebSocket, abie.mdc)`)
1798
+ return
1799
+ }
1800
+ passFn(`${relPkg}/k8s/ru/configmap-nginx.yaml: існує`)
1801
+ // kustomization resources must include configmap-nginx.yaml
1802
+ if (!RESOURCES_CONFIGMAP_NGINX_RE.test(ruRaw)) {
1803
+ fail(`${relRu}: у resources потрібен configmap-nginx.yaml (nginx-sidecar, abie.mdc)`)
1804
+ return
1805
+ }
1806
+ passFn(`${relRu}: resources містить configmap-nginx.yaml`)
1807
+ // Service -hl patch must include port: 8081 (proxy)
1808
+ const svcPatchByName = collectAbieRuServicePatchTextByTargetNameFromRaw(ruRaw)
1809
+ const hasHlWith8081 = [...svcPatchByName.entries()].some(
1810
+ ([name, pt]) => name.endsWith('-hl') && PATCH_PROXY_PORT_8081_RE.test(pt)
1811
+ )
1812
+ if (!hasHlWith8081) {
1813
+ fail(`${relRu}: у patch Service -hl потрібен port: 8081 (proxy) для nginx-sidecar (abie.mdc)`)
1814
+ return
1815
+ }
1816
+ passFn(`${relRu}: Service -hl patch містить port 8081 (nginx-sidecar)`)
1817
+ // Deployment patch must include nginx-sidecar (image nginx:*-alpine + containerPort: 8081)
1818
+ const deployPatches = collectDeploymentPatchTextsFromKustomization(ruRaw)
1819
+ const hasNginxSidecar = deployPatches.some(
1820
+ pt => NGINX_SIDECAR_IMAGE_RE.test(pt) && NGINX_SIDECAR_CONTAINER_PORT_RE.test(pt)
1821
+ )
1822
+ if (!hasNginxSidecar) {
1823
+ fail(
1824
+ `${relRu}: у patch Deployment потрібен nginx-sidecar (image nginx:…-alpine, containerPort: 8081) — abie.mdc`
1825
+ )
1826
+ return
1827
+ }
1828
+ passFn(`${relRu}: Deployment patch містить nginx-sidecar (image + containerPort 8081)`)
1829
+ // HTTPRoute patch must replace a backendRef port to 8081
1830
+ const combined = getCombinedNginxRunPatchTextFromKustomization(ruRaw)
1831
+ if (
1832
+ !HTTPROUTE_BACKENDREF_PORT_8081_RE.test(combined) &&
1833
+ !HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE.test(combined)
1834
+ ) {
1835
+ fail(
1836
+ `${relRu}: у patch HTTPRoute потрібен JSON6902 з path /spec/rules/…/backendRefs/…/port та value: 8081 (nginx-sidecar, abie.mdc)`
1837
+ )
1838
+ return
1839
+ }
1840
+ passFn(`${relRu}: HTTPRoute patch замінює порт на 8081 (nginx-sidecar)`)
1841
+ }
1842
+ }
1843
+
1630
1844
  /**
1631
1845
  * Перевіряє відповідність проєкту правилам abie.mdc.
1632
1846
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -1671,5 +1885,8 @@ export async function check() {
1671
1885
  await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
1672
1886
  }
1673
1887
 
1888
+ pass('Перевіряємо nginx-sidecar для Hasura WebSocket у ru (abie.mdc)')
1889
+ await ensureAbieNginxSidecarForHasura(root, yamlFiles, fail, pass)
1890
+
1674
1891
  return reporter.getExitCode()
1675
1892
  }