@nitra/cursor 1.8.189 → 1.8.191

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,23 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.191] - 2026-05-07
8
+
9
+ ### Added
10
+
11
+ - `check-npm-module.mjs`: перший заголовок **`## [version]`** у `npm/CHANGELOG.md` має збігатися з **`version`** у `npm/package.json` (найсвіжіший реліз зверху — Keep a Changelog); якщо є незакомічені зміни під **`npm/`**, `version` у робочому `npm/package.json` має відрізнятися від **`HEAD`** (інакше ризик дописати новий функціонал без bump).
12
+
13
+ ### Changed
14
+
15
+ - `npm-module` (mdc v1.9 → v1.10): розширено **«Build версія»** і **«CHANGELOG»** — чеклист для агента, заборона дописувати нові пункти в уже існуючу секцію релізу замість нового номера; впорядковано `CHANGELOG` (1.8.190 перед 1.8.189).
16
+
17
+ ## [1.8.190] - 2026-05-07
18
+
19
+ ### Added
20
+
21
+ - `js-run` (mdc v1.4 → v1.5): секція **`jsconfig.json`** — канонічний файл для backend-пакетів із каталогом **`src/`** (NodeNext, `include: ['src/**/*']`); для пакетів без `src/` вимога не діє.
22
+ - `check-js-run.mjs`: перевірка наявності та вмісту `jsconfig.json`, якщо в workspace-пакеті (без vite) є **`src/`**; тести у `check-js-run-fixture.test.mjs`.
23
+
7
24
  ## [1.8.189] - 2026-05-07
8
25
 
9
26
  ### Added
package/mdc/js-run.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
3
3
  alwaysApply: true
4
- version: '1.4'
4
+ version: '1.5'
5
5
  ---
6
6
 
7
7
  ## Область застосування
@@ -28,6 +28,25 @@ package.json
28
28
  readme.md
29
29
  ```
30
30
 
31
+ ## `jsconfig.json` (редактор / перевірка типів)
32
+
33
+ Якщо в **backend** workspace-пакеті (без `vite` у `devDependencies`) є каталог **`src/`**, у **корені цього пакета** має бути **`jsconfig.json`**. Якщо файлу ще немає — створи його з таким вмістом (канон js-run):
34
+
35
+ ```json title="jsconfig.json"
36
+ {
37
+ "compilerOptions": {
38
+ "lib": ["esnext"],
39
+ "module": "NodeNext",
40
+ "moduleResolution": "NodeNext",
41
+ "target": "esnext",
42
+ "checkJs": false
43
+ },
44
+ "include": ["src/**/*"]
45
+ }
46
+ ```
47
+
48
+ Якщо пакет не слідує структурі з `src/` (наприклад, лише `scripts/` у корені) — ця вимога не застосовується; для типових сервісів із `src/` файл обов’язковий і має збігатися з каноном.
49
+
31
50
  ## Використання @nitra/pino
32
51
 
33
52
  Проект використовує @nitra/pino для логування.
@@ -170,4 +189,4 @@ on:
170
189
 
171
190
  ## Перевірка
172
191
 
173
- `npx @nitra/cursor check js-run`
192
+ `npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Оформлення репозиторію для npm модуля
3
3
  alwaysApply: true
4
- version: '1.9'
4
+ version: '1.10'
5
5
  ---
6
6
 
7
7
  Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
@@ -47,12 +47,18 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
47
47
 
48
48
  У робочій копії не повинно бути більше одного незбереженого в **git** підвищення **build**-версії за раз.
49
49
 
50
+ **Чеклист у тому ж наборі змін, що й правки під `npm/`:** `version` у **`npm/package.json`** → **+1**; зверху **`npm/CHANGELOG.md`** нова секція **`## [нова версія] - …`**; у секції лише те, що входить у цей реліз.
51
+
52
+ **Антипатерн:** не дописувати нові bullet-и в уже існуючу секцію **`## [X.Y.Z]`**, якщо паралельно не піднімаєш **`version`** до нового номера й не створюєш **нову** секцію зверху. Інакше змішуються різні релізи в одному номері, а `check npm-module` / `check changelog` гірше ловлять порушення.
53
+
50
54
  **Підказка:** щоб не дублювати bump і бачити різницю зі збереженим деревом, перевір `git status npm/package.json` або `git diff HEAD -- npm/package.json` перед другим підвищенням у тій самій гілці / наборі змін.
51
55
 
52
56
  ## CHANGELOG
53
57
 
54
58
  Окреме правило **`changelog`** ([changelog.mdc](changelog.mdc)) вимагає `npm/CHANGELOG.md` із записом для поточної версії (Keep a Changelog) і присутність `"CHANGELOG.md"` у масиві `files` у `npm/package.json`. Логіка — PR-scoped (сума по гілці vs `dev`).
55
59
 
60
+ Найновіша версія — **перша** секція **`## [version]`** у файлі (зверху після заголовка). Вона **має збігатися** з полем **`version`** у **`npm/package.json`** — це перевіряє **`npx @nitra/cursor check npm-module`**.
61
+
56
62
  ## npm publish
57
63
 
58
64
  **`npm-publish.yml`:** push у **`main`**, **`on.push.paths`** з **`npm/**`**, **`JS-DevTools/npm-publish@v4.1.5`**, **`with.package: npm/package.json`**, **`permissions.id-token: write`** (OIDC на npm).
@@ -96,4 +102,4 @@ jobs:
96
102
 
97
103
  ## Перевірка
98
104
 
99
- `npx @nitra/cursor check npm-module`
105
+ `npx @nitra/cursor check npm-module` — зокрема узгодженість першої секції **`npm/CHANGELOG.md`** з **`version`** у **`npm/package.json`** і нагадування про bump при незакомічених змінах під **`npm/`** (через `git`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.189",
3
+ "version": "1.8.191",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -24,9 +24,11 @@
24
24
  * `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`);
25
25
  * - «Паузи через setTimeout»: `new Promise(resolve => setTimeout(resolve, ms))` (з/без `await`)
26
26
  * треба замінити на `await setTimeout(ms)` з `node:timers/promises`
27
- * (див. `utils/promise-settimeout-scan.mjs`).
27
+ * (див. `utils/promise-settimeout-scan.mjs`);
28
+ * - «jsconfig.json»: у backend-пакеті з каталогом `src/` у корені має бути `jsconfig.json`,
29
+ * вміст якого збігається з каноном js-run.mdc (NodeNext і include на дерево `src`).
28
30
  */
29
- import { existsSync } from 'node:fs'
31
+ import { existsSync, statSync } from 'node:fs'
30
32
  import { readFile } from 'node:fs/promises'
31
33
  import { join, relative } from 'node:path'
32
34
 
@@ -52,6 +54,99 @@ import {
52
54
  import { walkDir } from './utils/walkDir.mjs'
53
55
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
54
56
 
57
+ /** Канонічний `jsconfig.json` для backend workspace-пакетів із каталогом `src/` (js-run.mdc). */
58
+ const CANONICAL_BACKEND_JSCONFIG = Object.freeze({
59
+ compilerOptions: Object.freeze({
60
+ lib: Object.freeze(['esnext']),
61
+ module: 'NodeNext',
62
+ moduleResolution: 'NodeNext',
63
+ target: 'esnext',
64
+ checkJs: false
65
+ }),
66
+ include: Object.freeze(['src/**/*'])
67
+ })
68
+
69
+ /**
70
+ * Глибока рівність для JSON-подібних значень (масиви — порядок важливий).
71
+ * @param {unknown} a
72
+ * @param {unknown} b
73
+ * @returns {boolean}
74
+ */
75
+ function deepEqualJson(a, b) {
76
+ if (a === b) return true
77
+ if (a === null || b === null || typeof a !== typeof b) return false
78
+ if (typeof a !== 'object') return false
79
+ if (Array.isArray(a) !== Array.isArray(b)) return false
80
+ if (Array.isArray(a)) {
81
+ if (a.length !== b.length) return false
82
+ for (const [i, v] of a.entries()) {
83
+ if (!deepEqualJson(v, b[i])) return false
84
+ }
85
+ return true
86
+ }
87
+ const ao = /** @type {Record<string, unknown>} */ (a)
88
+ const bo = /** @type {Record<string, unknown>} */ (b)
89
+ const keysA = Object.keys(ao).sort()
90
+ const keysB = Object.keys(bo).sort()
91
+ if (keysA.length !== keysB.length) return false
92
+ for (const [i, k] of keysA.entries()) {
93
+ if (k !== keysB[i]) return false
94
+ if (!deepEqualJson(ao[k], bo[k])) return false
95
+ }
96
+ return true
97
+ }
98
+
99
+ /**
100
+ * Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
101
+ * @param {string} absPackageRoot абсолютний корінь пакета
102
+ * @returns {boolean}
103
+ */
104
+ function backendPackageHasSrcDir(absPackageRoot) {
105
+ const srcPath = join(absPackageRoot, 'src')
106
+ try {
107
+ return statSync(srcPath).isDirectory()
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Перевіряє `jsconfig.json` для backend-пакетів із `src/`.
115
+ * @param {string} rootDir відносний шлях workspace
116
+ * @param {string} absPackageRoot абсолютний корінь пакета
117
+ * @param {string} label префікс `[pkg] `
118
+ * @param {(msg: string) => void} fail
119
+ * @param {(msg: string) => void} passFn
120
+ * @returns {Promise<void>}
121
+ */
122
+ async function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
123
+ if (!backendPackageHasSrcDir(absPackageRoot)) return
124
+
125
+ const jcPath = join(rootDir, 'jsconfig.json')
126
+ if (!existsSync(jcPath)) {
127
+ fail(
128
+ `${label}є каталог src/, але немає jsconfig.json — додай канонічний файл з js-run.mdc ` +
129
+ `(NodeNext, include: src/**/*).`
130
+ )
131
+ return
132
+ }
133
+ let parsed
134
+ try {
135
+ parsed = JSON.parse(await readFile(jcPath, 'utf8'))
136
+ } catch {
137
+ fail(`${label}jsconfig.json не вдалося розпарсити як JSON`)
138
+ return
139
+ }
140
+ if (!deepEqualJson(parsed, CANONICAL_BACKEND_JSCONFIG)) {
141
+ fail(
142
+ `${label}jsconfig.json не збігається з каноном js-run.mdc — заміни на шаблон з правила ` +
143
+ `(compilerOptions: lib esnext, module/moduleResolution NodeNext, target esnext, checkJs false; include: src/**/*).`
144
+ )
145
+ return
146
+ }
147
+ passFn(`${label}jsconfig.json узгоджено з js-run (пакет з src/)`)
148
+ }
149
+
55
150
  /**
56
151
  * Перетворює абсолютний шлях у posix-формі відносно кореня пакета.
57
152
  * @param {string} absPackageRoot абсолютний корінь пакета
@@ -216,6 +311,8 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
216
311
  return
217
312
  }
218
313
 
314
+ await checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn)
315
+
219
316
  const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
220
317
  if (importViolations === 0) {
221
318
  passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
@@ -10,10 +10,16 @@
10
10
  * файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
11
11
  *
12
12
  * Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
13
+ *
14
+ * Версія та CHANGELOG: перший заголовок `## [version]` у `npm/CHANGELOG.md` має збігатися з `version` у
15
+ * `npm/package.json` (найсвіжіший реліз зверху). Якщо в git є незакомічені зміни під `npm/`, `version` у робочому
16
+ * файлі має відрізнятися від `HEAD` — інакше типовий пропуск bump після правок у пакеті.
13
17
  */
18
+ import { execFile } from 'node:child_process'
14
19
  import { existsSync } from 'node:fs'
15
20
  import { readFile, stat } from 'node:fs/promises'
16
21
  import { join } from 'node:path'
22
+ import { promisify } from 'node:util'
17
23
 
18
24
  import { createCheckReporter } from './utils/check-reporter.mjs'
19
25
  import {
@@ -26,8 +32,13 @@ import {
26
32
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
27
33
  import { walkDir } from './utils/walkDir.mjs'
28
34
 
35
+ const execFileAsync = promisify(execFile)
36
+
29
37
  const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
30
38
 
39
+ /** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
40
+ const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
41
+
31
42
  /** Канонічний entrypoint типів для пакетів із вихідним `.js` під каталогом `npm/src` */
32
43
  const TYPES_INDEX = './types/index.d.ts'
33
44
 
@@ -233,6 +244,122 @@ async function checkEmitTypesConfig(passFn, failFn) {
233
244
  * @param {(msg: string) => void} passFn callback при успішній перевірці
234
245
  * @param {(msg: string) => void} failFn callback при помилці
235
246
  */
247
+ /**
248
+ * Чи виконано `git` у корені робочого дерева.
249
+ * @returns {Promise<boolean>}
250
+ */
251
+ async function gitInsideWorkTree() {
252
+ try {
253
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' })
254
+ return stdout.trim() === 'true'
255
+ } catch {
256
+ return false
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Список незакомічених шляхів під `npm/` відносно `HEAD`.
262
+ * @returns {Promise<string[] | null>} шляхи або `null`, якщо `git` недоступний
263
+ */
264
+ async function gitDiffNameOnlyNpm() {
265
+ try {
266
+ const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD', '--', 'npm'], { encoding: 'utf8' })
267
+ return stdout.trim().split('\n').filter(Boolean)
268
+ } catch {
269
+ return null
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Поле `version` з `npm/package.json` на заданому git-ref (`HEAD:npm/package.json`).
275
+ * @param {string} refPath на кшталт `HEAD:npm/package.json`
276
+ * @returns {Promise<string | null>}
277
+ */
278
+ async function gitShowNpmPackageVersionAt(refPath) {
279
+ try {
280
+ const { stdout } = await execFileAsync('git', ['show', refPath], { encoding: 'utf8' })
281
+ const m = stdout.match(/"version":\s*"([^"]+)"/)
282
+ return m ? m[1] : null
283
+ } catch {
284
+ return null
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Версія з першого заголовка `## […]` у тексті CHANGELOG.
290
+ * @param {string} changelogText
291
+ * @returns {string | null}
292
+ */
293
+ function firstChangelogSectionVersion(changelogText) {
294
+ const m = changelogText.match(CHANGELOG_FIRST_VERSION_RE)
295
+ return m ? m[1] : null
296
+ }
297
+
298
+ /**
299
+ * Перший реліз у CHANGELOG має збігатися з `version` у `npm/package.json`.
300
+ * @param {(msg: string) => void} passFn
301
+ * @param {(msg: string) => void} failFn
302
+ * @returns {Promise<void>}
303
+ */
304
+ async function checkChangelogTopMatchesPackageVersion(passFn, failFn) {
305
+ if (!existsSync('npm/CHANGELOG.md') || !existsSync('npm/package.json')) return
306
+ const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
307
+ const ver = typeof pkg.version === 'string' ? pkg.version : null
308
+ if (!ver) {
309
+ failFn('npm/package.json: відсутнє поле version')
310
+ return
311
+ }
312
+ const cl = await readFile('npm/CHANGELOG.md', 'utf8')
313
+ const first = firstChangelogSectionVersion(cl)
314
+ if (!first) {
315
+ failFn('npm/CHANGELOG.md: не знайдено жодного заголовка ## [version]')
316
+ return
317
+ }
318
+ if (first !== ver) {
319
+ failFn(
320
+ `npm/CHANGELOG.md: перша секція [${first}] не збігається з npm/package.json version "${ver}" ` +
321
+ '(зверху має бути найсвіжіший реліз і той самий номер — npm-module.mdc).'
322
+ )
323
+ return
324
+ }
325
+ passFn(`npm/CHANGELOG.md: перша секція [${first}] збігається з npm/package.json`)
326
+ }
327
+
328
+ /**
329
+ * Незакомічені зміни під `npm/` вимагають підвищення `version` відносно `HEAD`.
330
+ * @param {(msg: string) => void} passFn
331
+ * @param {(msg: string) => void} failFn
332
+ * @returns {Promise<void>}
333
+ */
334
+ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
335
+ if (!(await gitInsideWorkTree())) {
336
+ passFn('npm-module: git недоступний або поза work tree — перевірку незакоміченого bump пропущено')
337
+ return
338
+ }
339
+ const changed = await gitDiffNameOnlyNpm()
340
+ if (changed === null) {
341
+ passFn('npm-module: git diff під npm/ недоступний — пропущено')
342
+ return
343
+ }
344
+ if (changed.length === 0) return
345
+
346
+ const headVer = await gitShowNpmPackageVersionAt('HEAD:npm/package.json')
347
+ if (headVer === null) return
348
+
349
+ const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
350
+ const cur = typeof pkg.version === 'string' ? pkg.version : null
351
+ if (!cur) return
352
+
353
+ if (cur === headVer) {
354
+ failFn(
355
+ `Незакомічені зміни під npm/ (${changed.join(', ')}), але "version" у npm/package.json лишився ${cur} ` +
356
+ '(як у HEAD). Підвищ version (+1) і додай секцію ## [нова версія] зверху CHANGELOG (npm-module.mdc).'
357
+ )
358
+ return
359
+ }
360
+ passFn(`npm/: незакомічені зміни під npm/ узгоджені з підвищенням version (${headVer} → ${cur})`)
361
+ }
362
+
236
363
  async function checkPublishWorkflow(passFn, failFn) {
237
364
  const publishWf = '.github/workflows/npm-publish.yml'
238
365
  if (!existsSync(publishWf)) {
@@ -371,5 +498,8 @@ export async function check() {
371
498
 
372
499
  await checkPublishWorkflow(pass, fail)
373
500
 
501
+ await checkChangelogTopMatchesPackageVersion(pass, fail)
502
+ await checkDirtyNpmRequiresVersionBump(pass, fail)
503
+
374
504
  return reporter.getExitCode()
375
505
  }