@nitra/cursor 1.8.88 → 1.8.94
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/README.md +1 -1
- package/mdc/npm-module.mdc +36 -1
- package/package.json +3 -1
- package/scripts/check-abie.mjs +51 -53
- package/scripts/check-k8s.mjs +36 -2
- package/scripts/check-npm-module.mjs +205 -1
- package/skills/lint/SKILL.md +42 -0
- package/types/bin/n-cursor.d.ts +2 -0
package/README.md
CHANGED
package/mdc/npm-module.mdc
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
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.
|
|
3
|
+
"version": "1.8.94",
|
|
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"
|
package/scripts/check-abie.mjs
CHANGED
|
@@ -543,7 +543,7 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
543
543
|
* Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
|
|
544
544
|
* @param {string} relFromRoot відносний шлях від кореня
|
|
545
545
|
* @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
|
|
546
|
-
* @returns {boolean}
|
|
546
|
+
* @returns {boolean} `true`, якщо шлях належить до base-шару abie
|
|
547
547
|
*/
|
|
548
548
|
export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
|
|
549
549
|
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
@@ -560,7 +560,7 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
560
560
|
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
|
|
561
561
|
* @param {unknown} obj корінь YAML
|
|
562
562
|
* @param {string} rel відносний шлях (повідомлення)
|
|
563
|
-
* @returns {{ refCount: number, errors: string[] }}
|
|
563
|
+
* @returns {{ refCount: number, errors: string[] }} кількість посилань і список порушень
|
|
564
564
|
*/
|
|
565
565
|
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
566
566
|
/** @type {string[]} */
|
|
@@ -582,26 +582,22 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
582
582
|
}
|
|
583
583
|
let refCount = 0
|
|
584
584
|
for (const rule of rules) {
|
|
585
|
-
if (rule
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
refCount++
|
|
602
|
-
const ns = brRec.namespace
|
|
603
|
-
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
604
|
-
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
585
|
+
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
586
|
+
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
587
|
+
if (Array.isArray(brs)) {
|
|
588
|
+
for (const br of brs) {
|
|
589
|
+
if (br !== null && typeof br === 'object' && !Array.isArray(br)) {
|
|
590
|
+
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
591
|
+
const name = brRec.name
|
|
592
|
+
if (typeof name === 'string' && ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
|
|
593
|
+
refCount++
|
|
594
|
+
const ns = brRec.namespace
|
|
595
|
+
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
596
|
+
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
605
601
|
}
|
|
606
602
|
}
|
|
607
603
|
}
|
|
@@ -613,7 +609,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
613
609
|
* @param {string} root корінь репозиторію
|
|
614
610
|
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
615
611
|
* @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
|
|
616
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>}
|
|
612
|
+
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
617
613
|
*/
|
|
618
614
|
export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
|
|
619
615
|
const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
@@ -622,34 +618,36 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
622
618
|
const baseErrors = []
|
|
623
619
|
for (const abs of yamlFilesAbs) {
|
|
624
620
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
621
|
+
if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
|
|
622
|
+
let raw
|
|
623
|
+
try {
|
|
624
|
+
raw = await readFile(abs, 'utf8')
|
|
625
|
+
} catch {
|
|
626
|
+
raw = undefined
|
|
627
|
+
}
|
|
628
|
+
if (raw !== undefined) {
|
|
629
|
+
const body = stripBom(raw)
|
|
630
|
+
const lines = body.split(/\r?\n/u)
|
|
631
|
+
const first = lines[0] ?? ''
|
|
632
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
633
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
634
|
+
let docs
|
|
635
|
+
try {
|
|
636
|
+
docs = parseAllDocuments(rest)
|
|
637
|
+
} catch {
|
|
638
|
+
docs = undefined
|
|
639
|
+
}
|
|
640
|
+
if (docs !== undefined) {
|
|
641
|
+
for (const doc of docs) {
|
|
642
|
+
if (doc.errors.length === 0) {
|
|
643
|
+
const obj = doc.toJSON()
|
|
644
|
+
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
645
|
+
refCount += st.refCount
|
|
646
|
+
baseErrors.push(...st.errors)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
648
650
|
}
|
|
649
|
-
const obj = doc.toJSON()
|
|
650
|
-
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
651
|
-
refCount += st.refCount
|
|
652
|
-
baseErrors.push(...st.errors)
|
|
653
651
|
}
|
|
654
652
|
}
|
|
655
653
|
return { refCount, baseErrors }
|
|
@@ -659,7 +657,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
659
657
|
* Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
|
|
660
658
|
* @param {string} combined сукупний текст patch **HTTPRoute**
|
|
661
659
|
* @param {'ua' | 'ru'} mode overlay
|
|
662
|
-
* @returns {number}
|
|
660
|
+
* @returns {number} кількість знайдених патчів namespace
|
|
663
661
|
*/
|
|
664
662
|
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
665
663
|
const re =
|
|
@@ -1073,8 +1071,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1073
1071
|
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
1074
1072
|
const sharedBackendAnalysisByPkg = new Map()
|
|
1075
1073
|
/**
|
|
1076
|
-
* @param {string} pkgAbs
|
|
1077
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>}
|
|
1074
|
+
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1075
|
+
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
1078
1076
|
*/
|
|
1079
1077
|
const getSharedBackendAnalysis = pkgAbs => {
|
|
1080
1078
|
let p = sharedBackendAnalysisByPkg.get(pkgAbs)
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -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,11 +1147,40 @@ 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
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
1156
|
+
* @returns {string | null} текст порушення або null
|
|
1157
|
+
*/
|
|
1158
|
+
export function healthCheckPolicyTargetRefHeadlessServiceViolation(manifest) {
|
|
1159
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
1160
|
+
return null
|
|
1161
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
1162
|
+
if (rec.kind !== 'HealthCheckPolicy') return null
|
|
1163
|
+
if (rec.apiVersion !== 'networking.gke.io/v1') return null
|
|
1164
|
+
const spec = rec.spec
|
|
1165
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
1166
|
+
const targetRef = /** @type {Record<string, unknown>} */ (spec).targetRef
|
|
1167
|
+
if (targetRef === null || targetRef === undefined || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
1168
|
+
return 'HealthCheckPolicy: потрібний spec.targetRef (див. k8s.mdc)'
|
|
1169
|
+
}
|
|
1170
|
+
const tr = /** @type {Record<string, unknown>} */ (targetRef)
|
|
1171
|
+
const k = tr.kind
|
|
1172
|
+
if (typeof k === 'string' && k !== '' && k !== 'Service') return null
|
|
1173
|
+
const n = tr.name
|
|
1174
|
+
if (typeof n !== 'string' || !n.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1175
|
+
return `HealthCheckPolicy: spec.targetRef.name має бути headless Service (суфікс «${SVC_HL_NAME_SUFFIX}»; див. k8s.mdc)`
|
|
1176
|
+
}
|
|
1177
|
+
return null
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1149
1180
|
/**
|
|
1150
1181
|
* Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
|
|
1151
1182
|
*
|
|
1152
1183
|
* Вимагає числовий **`port`**, щоб не плутати з **`HTTPHeaderMatch`** тощо (там теж є **`name`**, але без **`port`**).
|
|
1153
|
-
*
|
|
1154
1184
|
* @param {unknown} obj вузол у дереві **`spec`**
|
|
1155
1185
|
* @returns {boolean} true, якщо враховуємо поле **`name`** як посилання на Service
|
|
1156
1186
|
*/
|
|
@@ -1436,7 +1466,7 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
1436
1466
|
/**
|
|
1437
1467
|
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
|
|
1438
1468
|
* **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
|
|
1439
|
-
* (**headless**, суфікс **`-hl`** у **`metadata.name`**).
|
|
1469
|
+
* (**headless**, суфікс **`-hl`** у **`metadata.name`**), **HealthCheckPolicy** (**`targetRef.name`** з **`-hl`**).
|
|
1440
1470
|
* @param {string} rel відносний шлях
|
|
1441
1471
|
* @param {string} baseLower basename файлу (нижній регістр)
|
|
1442
1472
|
* @param {string} body вміст після modeline
|
|
@@ -1504,6 +1534,10 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
1504
1534
|
fail(`${rel}: Service (документ ${di + 1}): ${svcH}`)
|
|
1505
1535
|
}
|
|
1506
1536
|
}
|
|
1537
|
+
const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
|
|
1538
|
+
if (hcpHl !== null) {
|
|
1539
|
+
fail(`${rel}: документ ${di + 1}: ${hcpHl}`)
|
|
1540
|
+
}
|
|
1507
1541
|
}
|
|
1508
1542
|
}
|
|
1509
1543
|
}
|
|
@@ -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
|
|
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>} `true`, якщо знайдено хоча б один `.js`
|
|
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>} знайдений файл або `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 текст конфігурації hk
|
|
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 текст конфігурації hk
|
|
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 результат `JSON.parse` конфігурації
|
|
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 значення поля `types` з `package.json`
|
|
132
|
+
* @returns {string | null} абсолютний шлях або `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,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-lint
|
|
3
|
+
description: >-
|
|
4
|
+
Запустити кореневий bun run lint, виправити порушення й підтвердити чистий вихід
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# n-lint — лінт проєкту через кореневий скрипт
|
|
8
|
+
|
|
9
|
+
## Мета
|
|
10
|
+
|
|
11
|
+
Один раз узгоджено прогнати весь ланцюжок **`lint`** з кореневого **`package.json`**, усунути помилки (авто- та вручну) і переконатися, що **`bun run lint`** завершується з кодом **`0`**.
|
|
12
|
+
|
|
13
|
+
## Передумови
|
|
14
|
+
|
|
15
|
+
- Поточна робоча директорія — **корінь репозиторію**, де є **`package.json`** зі скриптом **`lint`**.
|
|
16
|
+
- Залежності встановлені (**`bun i`**) — якщо після правок змінювався **`package.json`** / lockfile, знову виконай **`bun i`** перед наступним запуском лінту.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. **Запуск** — виконай повний лінт:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun run lint
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. **Якщо exit code не 0** — проаналізуй вивід (останній упавший крок у ланцюжку **`lint`** часто видно з stderr / логів):
|
|
27
|
+
- Де скрипт уже робить **auto-fix** (**`--fix`**, **`markdownlint-cli2 --fix`**, **`oxfmt`** тощо) — перезапусти **`bun run lint`** після змін файлів.
|
|
28
|
+
- Де auto-fix **немає** (наприклад, **jscpd**, **cspell**, **zizmor**, перевірки без прапорця fix) — виправ код, конфіги або винятки **узгоджено з** `.cursor/rules/` (не розширюй ignore лише щоб приховати проблему без причини).
|
|
29
|
+
|
|
30
|
+
3. **Цикл** — повторюй кроки 1–2, доки **`bun run lint`** не завершиться успішно. Після суттєвих правок за потреби ще раз **`bun run lint`**, щоб переконатися, що не зламав наступний крок у скрипті **`lint`**.
|
|
31
|
+
|
|
32
|
+
4. **Верифікація** — фінальна перевірка (обов’язково з кодом **0**):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun run lint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
5. **Результат** — коротко опиши, що саме виправлено; якщо щось блокує нульовий exit code — залиш чітке пояснення й наступні кроки для людини.
|
|
39
|
+
|
|
40
|
+
## Примітка
|
|
41
|
+
|
|
42
|
+
Цей скіл **не** замінює **`npx @nitra/cursor check`**: **`lint`** перевіряє лінтери/формат у **`package.json`**, а **`check`** — програмні правила пакета **`@nitra/cursor`**. За потреби запускай обидва.
|