@nitra/cursor 1.8.88 → 1.8.93

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.
@@ -1,11 +1,46 @@
1
1
  ---
2
2
  description: Оформлення репозиторію для npm модуля
3
3
  alwaysApply: true
4
- version: '1.3'
4
+ version: '1.7'
5
5
  ---
6
6
 
7
7
  Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
8
8
 
9
+ ## TypeScript declaration (`npm/types`)
10
+
11
+ Файл **`npm/package.json`** має містити **`"types"`** (шлях до головного `.d.ts` або `.d.mts` під **`./types/…`**) і запис **`"types"`** у **`files`**, щоб npm публікував декларації.
12
+
13
+ Генерація — через **`tsc`** і **`bunx -p typescript`** (окремий пакет **`typescript`** у `devDependencies` не потрібен).
14
+
15
+ ### Варіант A: є вихідний **`.js`** під **`npm/src/`**
16
+
17
+ Якщо під **`npm/src`** (рекурсивно) є хоча б один файл **`.js`**:
18
+
19
+ - **`"types": "./types/index.d.ts"`**;
20
+ - у **hk** на **`pre-commit`** з каталогу **`npm/`** викликай:
21
+
22
+ ```bash
23
+ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types --skipLibCheck
24
+ ```
25
+
26
+ Якщо glob не розгортається — **`bash -O globstar`** або **`include`** у **`tsconfig`** з тими самими **`compilerOptions`**.
27
+
28
+ ### Варіант B: немає **`npm/src/**/*.js`** (наприклад лише **`bin/`**, **`scripts/**/*.mjs`**)
29
+
30
+ Не створюй штучний **`src/index.js`**. Замість цього:
31
+
32
+ 1. Додай **`npm/tsconfig.emit-types.json`** з **`include`** на реальні шляхи (`.js` / `.mjs`), **`compilerOptions`**: **`allowJs`**, **`declaration`**, **`emitDeclarationOnly`**, **`outDir`: `"types"`**, **`skipLibCheck`**: **`true`** (за потреби **`rootDir`**, **`module`**, **`moduleResolution`**).
33
+ 2. Після першого **`tsc`** подивись, який **`.d.ts`** / **`.d.mts`** відповідає публічному API, і вкажи його в **`types`** (наприклад **`./types/bin/cli.d.ts`**).
34
+ 3. У **hk** на **`pre-commit`** з **`npm/`** викликай **`tsc -p tsconfig.emit-types.json`**. Якщо через імпорти **TypeScript** все одно згенерує зайві **`.d.mts`** у **`types/`**, після **`tsc`** обрізай дерево до потрібного entrypoint (наприклад залиш лише **`types/bin/n-cursor.d.ts`** через **`find`**), щоб у пакеті не збирались декларації внутрішніх модулів.
35
+
36
+ Файл **`tsconfig.emit-types.json`** тримай у репозиторії для **hk** / локальної генерації; у **`files`** його не додавай, якщо не хочеш публікувати його на **npm**.
37
+
38
+ ## Git hooks: hk + pre-commit
39
+
40
+ У корені репозиторію — **`hk.pkl`** (або **`.config/hk.pkl`**) з **`["pre-commit"]`** і командою з відповідного варіанту (A або B) вище.
41
+
42
+ Після додавання **`hk.pkl`**: **`hk install`**.
43
+
9
44
  ## Build версія
10
45
 
11
46
  Після змін у **`npm/`** обовʼязково підвищ **build**-версію в **`npm/package.json`**, але не роби зайвих підвищень: між номером у файлі й тим, що вже збережено в **git** (`HEAD`), має лишатися не більше одного кроку **+1**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.88",
3
+ "version": "1.8.93",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -23,6 +23,7 @@
23
23
  "n-cursor": "./bin/n-cursor.js"
24
24
  },
25
25
  "files": [
26
+ "types",
26
27
  "mdc",
27
28
  "bin",
28
29
  "github-actions",
@@ -32,6 +33,7 @@
32
33
  "AGENTS.template.md"
33
34
  ],
34
35
  "type": "module",
36
+ "types": "./types/bin/n-cursor.d.ts",
35
37
  "scripts": {
36
38
  "test": "bun test tests",
37
39
  "rename-yaml-extensions": "bun ./bin/n-cursor.js rename-yaml-extensions"
@@ -34,6 +34,7 @@
34
34
  * кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
35
35
  * (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
36
36
  * посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
37
+ * **HealthCheckPolicy** (**`networking.gke.io/v1`**, GKE): **`spec.targetRef`** на **Service** — **`name`** з суфіксом **`-hl`** (див. k8s.mdc).
37
38
  * Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
38
39
  * **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
39
40
  * в **тому ж каталозі**, що й **`svc.yaml`** (логіка збігається з **`pathsFromKustomizationObject`**).
@@ -1146,6 +1147,37 @@ export function serviceSvcHlYamlHeadlessViolation(manifest) {
1146
1147
  return null
1147
1148
  }
1148
1149
 
1150
+ /**
1151
+ * Чи **HealthCheckPolicy** (GKE) у **`spec.targetRef`** посилається на headless **Service** (суфікс **`-hl`**).
1152
+ *
1153
+ * Застосовується лише для **`apiVersion: networking.gke.io/v1`** і **`targetRef.kind: Service`** (або без **`kind`**).
1154
+ * Інші **`targetRef.kind`** скрипт не оцінює.
1155
+ *
1156
+ * @param {unknown} manifest корінь YAML-документа
1157
+ * @returns {string | null} текст порушення або null
1158
+ */
1159
+ export function healthCheckPolicyTargetRefHeadlessServiceViolation(manifest) {
1160
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
1161
+ return null
1162
+ const rec = /** @type {Record<string, unknown>} */ (manifest)
1163
+ if (rec.kind !== 'HealthCheckPolicy') return null
1164
+ if (rec.apiVersion !== 'networking.gke.io/v1') return null
1165
+ const spec = rec.spec
1166
+ if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
1167
+ const targetRef = /** @type {Record<string, unknown>} */ (spec).targetRef
1168
+ if (targetRef === null || targetRef === undefined || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
1169
+ return 'HealthCheckPolicy: потрібний spec.targetRef (див. k8s.mdc)'
1170
+ }
1171
+ const tr = /** @type {Record<string, unknown>} */ (targetRef)
1172
+ const k = tr.kind
1173
+ if (typeof k === 'string' && k !== '' && k !== 'Service') return null
1174
+ const n = tr.name
1175
+ if (typeof n !== 'string' || !n.endsWith(SVC_HL_NAME_SUFFIX)) {
1176
+ return `HealthCheckPolicy: spec.targetRef.name має бути headless Service (суфікс «${SVC_HL_NAME_SUFFIX}»; див. k8s.mdc)`
1177
+ }
1178
+ return null
1179
+ }
1180
+
1149
1181
  /**
1150
1182
  * Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
1151
1183
  *
@@ -1436,7 +1468,7 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
1436
1468
  /**
1437
1469
  * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
1438
1470
  * **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
1439
- * (**headless**, суфікс **`-hl`** у **`metadata.name`**).
1471
+ * (**headless**, суфікс **`-hl`** у **`metadata.name`**), **HealthCheckPolicy** (**`targetRef.name`** з **`-hl`**).
1440
1472
  * @param {string} rel відносний шлях
1441
1473
  * @param {string} baseLower basename файлу (нижній регістр)
1442
1474
  * @param {string} body вміст після modeline
@@ -1504,6 +1536,10 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
1504
1536
  fail(`${rel}: Service (документ ${di + 1}): ${svcH}`)
1505
1537
  }
1506
1538
  }
1539
+ const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
1540
+ if (hcpHl !== null) {
1541
+ fail(`${rel}: документ ${di + 1}: ${hcpHl}`)
1542
+ }
1507
1543
  }
1508
1544
  }
1509
1545
  }
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * Перевіряє структуру npm-модуля в монорепо за правилом npm-module.mdc.
3
3
  *
4
- * Workspace `npm/`, `npm/package.json`, workflow `npm-publish.yml` з OIDC, `on.push.paths` з glob для каталогу npm (див. npm-module.mdc).
4
+ * Workspace `npm/`, `npm/package.json`, workflow `npm-publish.yml` з OIDC, `on.push.paths` з glob для каталогу npm.
5
+ *
6
+ * Якщо під `npm/src` є хоча б один файл `.js`, очікується канонічний layout: `types` → `./types/index.d.ts`,
7
+ * згенерований `index.d.ts` у `npm/types/`, і hk з викликом `tsc` по файлах під `npm/src`.
8
+ *
9
+ * Якщо таких файлів немає — layout через `npm/tsconfig.emit-types.json`: поле `types` має вказувати на існуючий
10
+ * файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
11
+ *
5
12
  * Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
6
13
  */
7
14
  import { existsSync } from 'node:fs'
8
15
  import { readFile, stat } from 'node:fs/promises'
16
+ import { join } from 'node:path'
9
17
 
10
18
  import { createCheckReporter } from './utils/check-reporter.mjs'
11
19
  import {
@@ -15,6 +23,121 @@ import {
15
23
  pushHasMainBranch,
16
24
  pushPathsIncludeNpmGlob
17
25
  } from './utils/gha-workflow.mjs'
26
+ import { walkDir } from './utils/walkDir.mjs'
27
+
28
+ /** Канонічний entrypoint типів для пакетів із вихідним `.js` під каталогом `npm/src` */
29
+ const TYPES_INDEX = './types/index.d.ts'
30
+
31
+ /** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
32
+ const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
33
+
34
+ /**
35
+ * Чи є під `npm/src` хоча б один `.js` (рекурсивно).
36
+ * @returns {Promise<boolean>}
37
+ */
38
+ async function npmSrcTreeHasJsFile() {
39
+ const root = 'npm/src'
40
+ if (!existsSync(root)) {
41
+ return false
42
+ }
43
+ let found = false
44
+ await walkDir(root, p => {
45
+ if (p.endsWith('.js')) {
46
+ found = true
47
+ }
48
+ })
49
+ return found
50
+ }
51
+
52
+ /**
53
+ * Знаходить текстовий вміст конфігурації hk для перевірки npm-module.
54
+ * @returns {Promise<{ path: string, text: string } | null>}
55
+ */
56
+ async function readHkConfig() {
57
+ const candidates = ['hk.pkl', '.config/hk.pkl']
58
+ for (const p of candidates) {
59
+ if (existsSync(p)) {
60
+ const text = await readFile(p, 'utf8')
61
+ return { path: p, text }
62
+ }
63
+ }
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * Підрядки для hk при layout з каталогом `npm/src` і glob `src` + `.js` у команді (див. npm-module.mdc).
69
+ * @param {string} hkText
70
+ * @returns {string[]}
71
+ */
72
+ function missingHkSrcLayoutFragments(hkText) {
73
+ const need = [
74
+ '["pre-commit"]',
75
+ 'bunx -p typescript tsc',
76
+ 'src/**/*.js',
77
+ '--declaration',
78
+ '--allowJs',
79
+ '--emitDeclarationOnly',
80
+ '--outDir types',
81
+ '--skipLibCheck'
82
+ ]
83
+ return need.filter(s => !hkText.includes(s))
84
+ }
85
+
86
+ /**
87
+ * Підрядки для hk при layout з `tsconfig.emit-types.json` (див. npm-module.mdc).
88
+ * @param {string} hkText
89
+ * @returns {string[]}
90
+ */
91
+ function missingHkEmitTypesConfigFragments(hkText) {
92
+ const need = ['["pre-commit"]', 'bunx -p typescript tsc', 'tsconfig.emit-types.json']
93
+ return need.filter(s => !hkText.includes(s))
94
+ }
95
+
96
+ /**
97
+ * Перевіряє `npm/tsconfig.emit-types.json` на мінімальний набір опцій для `emitDeclarationOnly` у `types/`.
98
+ * @param {unknown} parsed
99
+ * @returns {string[]} повідомлення про помилки (порожній — OK)
100
+ */
101
+ function emitTypesConfigIssues(parsed) {
102
+ const issues = []
103
+ if (!parsed || typeof parsed !== 'object') {
104
+ return ['некоректний JSON']
105
+ }
106
+ const co = /** @type {{ [k: string]: unknown }} */ (parsed).compilerOptions
107
+ if (!co || typeof co !== 'object') {
108
+ return ['відсутній compilerOptions']
109
+ }
110
+ const get = k => /** @type {{ [k: string]: unknown }} */ (co)[k]
111
+ if (get('allowJs') !== true) {
112
+ issues.push('compilerOptions.allowJs має бути true')
113
+ }
114
+ if (get('declaration') !== true) {
115
+ issues.push('compilerOptions.declaration має бути true')
116
+ }
117
+ if (get('emitDeclarationOnly') !== true) {
118
+ issues.push('compilerOptions.emitDeclarationOnly має бути true')
119
+ }
120
+ if (get('outDir') !== 'types') {
121
+ issues.push('compilerOptions.outDir має бути "types"')
122
+ }
123
+ if (get('skipLibCheck') !== true) {
124
+ issues.push('compilerOptions.skipLibCheck має бути true')
125
+ }
126
+ return issues
127
+ }
128
+
129
+ /**
130
+ * Шлях на дискі до файлу з поля `types` у `npm/package.json` (значення на кшталт `./types/bin/x.d.ts`).
131
+ * @param {string} typesField
132
+ * @returns {string | null}
133
+ */
134
+ function npmTypesFileFromPackageField(typesField) {
135
+ if (typeof typesField !== 'string' || !typesField.startsWith('./types/')) {
136
+ return null
137
+ }
138
+ const rel = typesField.slice(2)
139
+ return join('npm', rel)
140
+ }
18
141
 
19
142
  /**
20
143
  * Перевіряє відповідність проєкту правилам npm-module.mdc
@@ -57,6 +180,87 @@ export async function check() {
57
180
  fail('npm/package.json не існує — створи package.json для npm модуля')
58
181
  }
59
182
 
183
+ const useSrcJsLayout = await npmSrcTreeHasJsFile()
184
+
185
+ if (existsSync('npm/package.json')) {
186
+ const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
187
+ const typesField = npmPkg.types
188
+
189
+ if (useSrcJsLayout) {
190
+ if (typesField === TYPES_INDEX) {
191
+ pass(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
192
+ } else {
193
+ fail(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
194
+ }
195
+ } else {
196
+ if (typeof typesField === 'string' && /^\.\/types\/.+\.d\.(ts|mts)$/.test(typesField)) {
197
+ pass(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
198
+ } else {
199
+ fail(
200
+ 'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
201
+ )
202
+ }
203
+ }
204
+
205
+ const files = npmPkg.files
206
+ if (Array.isArray(files) && files.includes('types')) {
207
+ pass('npm/package.json: files містить "types"')
208
+ } else {
209
+ fail('npm/package.json: масив files має містити "types"')
210
+ }
211
+
212
+ const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
213
+ if (typesPath && existsSync(typesPath)) {
214
+ pass(`${typesPath} існує`)
215
+ } else {
216
+ fail(
217
+ useSrcJsLayout
218
+ ? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
219
+ : `Файл для поля types не знайдено або шлях не під ./types/ — ${String(typesField)}`
220
+ )
221
+ }
222
+ }
223
+
224
+ if (!useSrcJsLayout) {
225
+ if (existsSync(EMIT_TYPES_CONFIG)) {
226
+ pass(`${EMIT_TYPES_CONFIG} існує`)
227
+ let raw
228
+ try {
229
+ raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
230
+ } catch {
231
+ fail(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
232
+ raw = null
233
+ }
234
+ if (raw) {
235
+ const issues = emitTypesConfigIssues(raw)
236
+ if (issues.length === 0) {
237
+ pass(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
238
+ } else {
239
+ fail(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
240
+ }
241
+ }
242
+ } else {
243
+ fail(
244
+ `Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
245
+ )
246
+ }
247
+ }
248
+
249
+ const hk = await readHkConfig()
250
+ if (hk) {
251
+ pass(`${hk.path} існує`)
252
+ const missing = useSrcJsLayout ? missingHkSrcLayoutFragments(hk.text) : missingHkEmitTypesConfigFragments(hk.text)
253
+ if (missing.length === 0) {
254
+ pass(
255
+ `${hk.path}: pre-commit містить очікуваний виклик tsc (${useSrcJsLayout ? 'layout src' : 'tsconfig emit-types'})`
256
+ )
257
+ } else {
258
+ fail(`${hk.path}: онови pre-commit крок (npm-module.mdc); не знайдено: ${missing.join(', ')}`)
259
+ }
260
+ } else {
261
+ fail('Очікується hk.pkl або .config/hk.pkl з pre-commit і tsc (npm-module.mdc)')
262
+ }
263
+
60
264
  if (existsSync('.github/workflows')) {
61
265
  pass('.github/workflows/ існує')
62
266
  } else {
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};