@nitra/cursor 1.21.0 → 1.22.0
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 +14 -0
- package/bin/n-cursor.js +21 -2
- package/package.json +1 -1
- package/scripts/lib/run-lint-cli.mjs +118 -0
- package/scripts/lib/timing-summary.mjs +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.22.0] - 2026-05-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`npx @nitra/cursor lint`** — оркестратор лінт-ланцюжка з тайменгом на кожен крок. Послідовно запускає присутні у root `package.json` скрипти з фіксованого списку (`lint-ga`, `lint-js`, `lint-rego`, `lint-style`, `lint-text`, `lint-security`, `oxfmt`), **fail-fast** на першому ненульовому exit-коді. Наприкінці друкує таблицю `⏱ Lint timing` з часом кожного кроку — для атрибуції повільних кроків замість анонімного `&&`-агрегатора.
|
|
12
|
+
- **`runFixCommand` тепер друкує `⏱ Fix timing`** після прогону всіх `rules/<id>/fix.mjs` — per-rule час + сума. Маркер `❌` на впалих рядках.
|
|
13
|
+
- `npm/scripts/lib/timing-summary.mjs` — чистий форматер `formatTimingSummary(title, entries)` (спільний для fix і lint). 9 тестів у `tests/timing-summary.test.mjs`.
|
|
14
|
+
- `npm/scripts/lib/run-lint-cli.mjs` — `runLintCli({ cwd, spawnSyncFn, now, log, logError })` з DI для юніт-тестів. 7 тестів у `tests/run-lint-cli.test.mjs`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Кореневий `package.json` цього монорепо: `lint` → `n-cursor lint`; додано окремий скрипт `oxfmt: "oxfmt ."`, який раніше йшов у хвості ланцюжка прямою командою.
|
|
19
|
+
- Скіли `/n-fix` і `/n-lint`: додано вимогу копіювати таблицю `⏱` з виводу інструмента у фінальне резюме відповіді користувачу.
|
|
20
|
+
|
|
7
21
|
## [1.21.0] - 2026-05-25
|
|
8
22
|
|
|
9
23
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* дістає `tool_input.file_path`, маршрутизує його у відповідні правила
|
|
14
14
|
* (`*.mjs` → `js-lint`, `*.vue` → `js-lint style-lint vue` тощо) і викликає
|
|
15
15
|
* `fix` лише з ними. Прописується автоматично в `.claude/settings.json`.
|
|
16
|
+
* `npx \@nitra/cursor lint` — оркестратор lint-ланцюжка з кореневого `package.json` з тайменгом
|
|
17
|
+
* кожного `lint-*` / `oxfmt` скрипта (fail-fast); канонічна заміна
|
|
18
|
+
* раніше ручного `lint-ga && lint-js && …` агрегатора.
|
|
16
19
|
* `npx \@nitra/cursor lint-ga` — канонічний lint-ga (ga.mdc): preflight на `shellcheck` →
|
|
17
20
|
* `bunx github-actionlint` → `uvx zizmor --offline --collect=workflows .`
|
|
18
21
|
* `npx \@nitra/cursor lint-rego` — канонічний lint-rego (conftest.mdc + rego.mdc):
|
|
@@ -99,6 +102,8 @@ import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitr
|
|
|
99
102
|
import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
100
103
|
import { runSkillsCli } from '../scripts/skills-cli.mjs'
|
|
101
104
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
105
|
+
import { runLintCli } from '../scripts/lib/run-lint-cli.mjs'
|
|
106
|
+
import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
|
|
102
107
|
|
|
103
108
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
104
109
|
const CONFIG_FILE = '.n-cursor.json'
|
|
@@ -1183,12 +1188,19 @@ async function runFixCommand(requestedRules) {
|
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1185
1190
|
let totalFailed = 0
|
|
1191
|
+
/** @type {{ id: string, ms: number, ok: boolean }[]} */
|
|
1192
|
+
const timings = []
|
|
1186
1193
|
for (const id of idsToRun) {
|
|
1187
1194
|
const fixPath = join(BUNDLED_RULES_DIR, id, 'fix.mjs')
|
|
1195
|
+
const startedAt = Date.now()
|
|
1188
1196
|
const result = spawnSync('bun', [fixPath], { stdio: 'inherit' })
|
|
1189
|
-
|
|
1197
|
+
const ok = result.status === 0
|
|
1198
|
+
timings.push({ id: `fix-${id}`, ms: Date.now() - startedAt, ok })
|
|
1199
|
+
if (!ok) totalFailed++
|
|
1190
1200
|
}
|
|
1191
1201
|
|
|
1202
|
+
process.stdout.write(formatTimingSummary('Fix timing', timings))
|
|
1203
|
+
|
|
1192
1204
|
if (totalFailed > 0) {
|
|
1193
1205
|
throw new Error(`${totalFailed} з ${idsToRun.length} правил мають проблеми`)
|
|
1194
1206
|
}
|
|
@@ -1437,6 +1449,13 @@ try {
|
|
|
1437
1449
|
|
|
1438
1450
|
break
|
|
1439
1451
|
}
|
|
1452
|
+
case 'lint': {
|
|
1453
|
+
// Оркестратор lint-ланцюжка з тайменгом на кожен крок (fail-fast).
|
|
1454
|
+
// Замінює раніше використовуваний агрегатор `bun run lint-ga && bun run lint-js && …` у root package.json.
|
|
1455
|
+
process.exitCode = runLintCli()
|
|
1456
|
+
|
|
1457
|
+
break
|
|
1458
|
+
}
|
|
1440
1459
|
case 'lint-ga': {
|
|
1441
1460
|
// Канонічний lint-ga з preflight на shellcheck → actionlint → zizmor → check-ga (ga.mdc).
|
|
1442
1461
|
// Останній крок (check-ga) async — тому await обов'язковий, інакше process.exitCode буде Promise.
|
|
@@ -1490,7 +1509,7 @@ try {
|
|
|
1490
1509
|
default: {
|
|
1491
1510
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1492
1511
|
console.error(
|
|
1493
|
-
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, skill`
|
|
1512
|
+
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, skill`
|
|
1494
1513
|
)
|
|
1495
1514
|
process.exitCode = 1
|
|
1496
1515
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor lint` — оркестратор лінт-ланцюжка з тайменгом на кожен крок.
|
|
3
|
+
*
|
|
4
|
+
* Замість агрегатора `bun run lint-ga && bun run lint-js && ... && oxfmt .` у кореневому
|
|
5
|
+
* `package.json` (де child-процеси анонімні і час кожного не видно), цей орекстратор:
|
|
6
|
+
*
|
|
7
|
+
* - читає `scripts` з кореневого `package.json`,
|
|
8
|
+
* - бере **присутні** ключі з фіксованого списку `LINT_SCRIPTS` (відсутні мовчки пропускає),
|
|
9
|
+
* - послідовно запускає `bun run <script>`,
|
|
10
|
+
* - заміряє час кожного,
|
|
11
|
+
* - **fail-fast**: при першому ненульовому exit-коді зупиняється, друкує таблицю
|
|
12
|
+
* лише по виконаних і повертає той самий код,
|
|
13
|
+
* - друкує підсумкову таблицю `⏱ Lint timing` і повертає 0, якщо все ОК.
|
|
14
|
+
*
|
|
15
|
+
* Список + порядок зумисне фіксований: збігається з канонічним ланцюжком, що його раніше
|
|
16
|
+
* тримав root `package.json`. Динамічний discovery (`scripts/^lint-/`) дав би непередбачуваний
|
|
17
|
+
* порядок і небажану інтерпретацію кастомних `lint-*` користувача.
|
|
18
|
+
*
|
|
19
|
+
* `oxfmt` — окремий рядок поза префіксом `lint-`, ставиться в кінець (як було у `lint`).
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync as defaultSpawnSync } from 'node:child_process'
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
|
|
25
|
+
import { formatTimingSummary } from './timing-summary.mjs'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Імена npm-скриптів, які `n-cursor lint` запускає **по черзі**, якщо вони є у root `package.json`.
|
|
29
|
+
* Порядок дзеркалить попередній агрегатор `lint`: cheap-checks першими, формат — в кінці.
|
|
30
|
+
*/
|
|
31
|
+
export const LINT_SCRIPTS = /** @type {const} */ ([
|
|
32
|
+
'lint-ga',
|
|
33
|
+
'lint-js',
|
|
34
|
+
'lint-rego',
|
|
35
|
+
'lint-style',
|
|
36
|
+
'lint-text',
|
|
37
|
+
'lint-security',
|
|
38
|
+
'oxfmt'
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Читає `scripts` з `package.json` у заданій теці. Повертає `null`, якщо файла немає, JSON
|
|
43
|
+
* некоректний або поля `scripts` нема. Не кидає — викликач сам вирішує, що робити.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} root абсолютний шлях до теки з `package.json`
|
|
46
|
+
* @returns {Record<string, string> | null} мапа scripts або null
|
|
47
|
+
*/
|
|
48
|
+
function readRootScripts(root) {
|
|
49
|
+
const packageJsonPath = join(root, 'package.json')
|
|
50
|
+
if (!existsSync(packageJsonPath)) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
55
|
+
const scripts = parsed?.scripts
|
|
56
|
+
if (!scripts || typeof scripts !== 'object') {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
return /** @type {Record<string, string>} */ (scripts)
|
|
60
|
+
} catch {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {{
|
|
67
|
+
* cwd?: string,
|
|
68
|
+
* spawnSyncFn?: typeof defaultSpawnSync,
|
|
69
|
+
* now?: () => number,
|
|
70
|
+
* log?: (text: string) => void,
|
|
71
|
+
* logError?: (text: string) => void
|
|
72
|
+
* }} RunLintCliOptions
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Виконує лінт-ланцюжок з тайменгом. Повертає exit-код, не кидає винятків (для прямого
|
|
77
|
+
* присвоєння у `process.exitCode`).
|
|
78
|
+
*
|
|
79
|
+
* @param {RunLintCliOptions} [options] DI для тестів (мокаємо spawn / fs / clock)
|
|
80
|
+
* @returns {number} 0 = успіх, ненульовий = code першого впалого скрипта, або 1 при структурних проблемах
|
|
81
|
+
*/
|
|
82
|
+
export function runLintCli(options = {}) {
|
|
83
|
+
const root = options.cwd ?? process.cwd()
|
|
84
|
+
const spawnSync = options.spawnSyncFn ?? defaultSpawnSync
|
|
85
|
+
const now = options.now ?? Date.now
|
|
86
|
+
const log = options.log ?? (text => process.stdout.write(text))
|
|
87
|
+
const logError = options.logError ?? (text => process.stderr.write(text))
|
|
88
|
+
|
|
89
|
+
const scripts = readRootScripts(root)
|
|
90
|
+
if (scripts === null) {
|
|
91
|
+
logError(`❌ n-cursor lint: не знайдено package.json або поля "scripts" у ${root}\n`)
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const present = LINT_SCRIPTS.filter(name => typeof scripts[name] === 'string' && scripts[name].length > 0)
|
|
96
|
+
if (present.length === 0) {
|
|
97
|
+
log('\nℹ️ n-cursor lint: у package.json немає жодного з lint-* / oxfmt скриптів — нічого запускати.\n')
|
|
98
|
+
return 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {{ id: string, ms: number, ok: boolean }[]} */
|
|
102
|
+
const timings = []
|
|
103
|
+
let failedCode = 0
|
|
104
|
+
for (const name of present) {
|
|
105
|
+
const startedAt = now()
|
|
106
|
+
const result = spawnSync('bun', ['run', name], { stdio: 'inherit', cwd: root })
|
|
107
|
+
const code = typeof result.status === 'number' ? result.status : 1
|
|
108
|
+
const ok = code === 0
|
|
109
|
+
timings.push({ id: name, ms: now() - startedAt, ok })
|
|
110
|
+
if (!ok) {
|
|
111
|
+
failedCode = code === 0 ? 1 : code
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
log(formatTimingSummary('Lint timing', timings))
|
|
117
|
+
return failedCode
|
|
118
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Формат таблиці-резюме часу виконання для оркестраторів `fix` / `lint`.
|
|
3
|
+
*
|
|
4
|
+
* Дві спільні точки використання:
|
|
5
|
+
* - `runFixCommand` у `bin/n-cursor.js` — після прогону всіх `rules/<id>/fix.mjs`.
|
|
6
|
+
* - `runLintCli` у `scripts/lib/run-lint-cli.mjs` — після прогону `lint-*` скриптів з кореневого `package.json`.
|
|
7
|
+
*
|
|
8
|
+
* Чиста функція без I/O — повертає готовий рядок (з фінальним `\n`), друк — на стороні виклику.
|
|
9
|
+
* Час виводиться як `<ціла>.<десята>s`, навіть для субсекундних інтервалів — щоб одиниця була стабільна.
|
|
10
|
+
*
|
|
11
|
+
* Маркер `❌` на рядку — якщо `ok === false`.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {{ id: string, ms: number, ok: boolean }} TimingEntry
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {string} символ горизонтальної риски між списком і `total` */
|
|
17
|
+
const RULER = '─'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Форматує мілісекунди як `<sec>.<десята>s`. Округлення до десятої — нижнє (floor), щоб
|
|
21
|
+
* однаковий ms давав однаковий вивід у різних таблицях незалежно від платформи (Number.prototype.toFixed
|
|
22
|
+
* робить round-half-to-even, що для 950ms дає `0.9s` — приймаємо).
|
|
23
|
+
*
|
|
24
|
+
* @param {number} ms тривалість у мілісекундах (>= 0)
|
|
25
|
+
* @returns {string} наприклад `0.0s`, `1.2s`, `12.3s`
|
|
26
|
+
*/
|
|
27
|
+
export function formatDurationMs(ms) {
|
|
28
|
+
const seconds = Math.max(0, ms) / 1000
|
|
29
|
+
return `${seconds.toFixed(1)}s`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Рендерить таблицю-резюме у вигляді багаторядкового тексту, готового до stdout.
|
|
34
|
+
*
|
|
35
|
+
* Структура:
|
|
36
|
+
*
|
|
37
|
+
* ```
|
|
38
|
+
* ⏱ <title>:
|
|
39
|
+
* <id> <duration> [❌]
|
|
40
|
+
* ...
|
|
41
|
+
* ──────────────
|
|
42
|
+
* total <sum>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Ширина колонки id вирівнюється під найдовший id у списку. Мінімальна ширина riski — 14
|
|
46
|
+
* (узгоджено з типовою довжиною заголовків `fix-js-lint` / `lint-security`).
|
|
47
|
+
*
|
|
48
|
+
* @param {string} title заголовок таблиці (наприклад, `Fix timing` або `Lint timing`)
|
|
49
|
+
* @param {TimingEntry[]} timings записи в порядку запуску — друкуються як є, не сортуються
|
|
50
|
+
* @returns {string} готовий до stdout текст з кінцевим `\n`
|
|
51
|
+
*/
|
|
52
|
+
export function formatTimingSummary(title, timings) {
|
|
53
|
+
if (timings.length === 0) {
|
|
54
|
+
return ''
|
|
55
|
+
}
|
|
56
|
+
const idWidth = Math.max(14, ...timings.map(t => t.id.length))
|
|
57
|
+
const lines = [`\n⏱ ${title}:`]
|
|
58
|
+
let totalMs = 0
|
|
59
|
+
for (const { id, ms, ok } of timings) {
|
|
60
|
+
totalMs += ms
|
|
61
|
+
const failMark = ok ? '' : ' ❌'
|
|
62
|
+
lines.push(` ${id.padEnd(idWidth)} ${formatDurationMs(ms)}${failMark}`)
|
|
63
|
+
}
|
|
64
|
+
lines.push(` ${RULER.repeat(idWidth + 2 + 6)}`)
|
|
65
|
+
lines.push(` ${'total'.padEnd(idWidth)} ${formatDurationMs(totalMs)}`)
|
|
66
|
+
return `${lines.join('\n')}\n`
|
|
67
|
+
}
|