@nitra/cursor 1.8.191 → 1.8.192
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 +11 -0
- package/mdc/text.mdc +11 -7
- package/package.json +1 -1
- package/scripts/check-text.mjs +6 -5
- package/scripts/run-shellcheck-text.mjs +193 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
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.192] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `run-shellcheck-text.mjs`: для `lint-text` — перевірка наявності `shellcheck`/`patch`, авто-виправлення через `shellcheck -f diff` + `patch -p1`, фінальний прогін по tracked `*.sh` (git) або `**/*.sh` без `node_modules`.
|
|
12
|
+
- `text` (mdc v1.25 → v1.26): **shellcheck** у ланцюжку `lint-text`, рекомендація **`timonwong.shellcheck`**, тригер workflow **`**/*.sh`**; тести `run-shellcheck-text.test.mjs`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `check-text.mjs`: `lint-text` має містити `run-shellcheck-text.mjs`; `extensions.json` — `timonwong.shellcheck`.
|
|
17
|
+
|
|
7
18
|
## [1.8.191] - 2026-05-07
|
|
8
19
|
|
|
9
20
|
### Added
|
package/mdc/text.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Обробка та перевірка текстових файлів, oxfmt, cspell, markdownlint-cli2, v8r, CI
|
|
2
|
+
description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), markdownlint-cli2, v8r, CI
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.26'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint**, workflow **`lint-text`**.
|
|
7
|
+
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -13,6 +13,7 @@ version: '1.25'
|
|
|
13
13
|
"github.vscode-github-actions",
|
|
14
14
|
"oxc.oxc-vscode",
|
|
15
15
|
"DavidAnson.vscode-markdownlint",
|
|
16
|
+
"timonwong.shellcheck",
|
|
16
17
|
"redhat.vscode-yaml",
|
|
17
18
|
"irongeek.vscode-env"
|
|
18
19
|
]
|
|
@@ -112,12 +113,14 @@ version: '1.25'
|
|
|
112
113
|
|
|
113
114
|
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`** (**`^2.0.0`** або новіший у лінії 2.x) — з **2.0.0** у пакет транзитивно входять типові **`@cspell/dict-*`**, тому **не** додавай їх окремо в корінь. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`bunx v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
|
|
114
115
|
|
|
116
|
+
**shellcheck:** інструмент лише в **`PATH`** (як у **ga.mdc** для `lint-ga`), **не** додавай до `dependencies` / `devDependencies`. Якщо `shellcheck` відсутній — встанови локально (**macOS:** `brew install shellcheck`; **Debian/Ubuntu:** `sudo apt-get install -y shellcheck`; **Arch:** `sudo pacman -S shellcheck`). У **`lint-text`** після **`cspell`** викликай **`bun ./npm/scripts/run-shellcheck-text.mjs`** (у споживачі після синку — `node_modules/@nitra/cursor/scripts/run-shellcheck-text.mjs`): під капотом циклічно **`shellcheck -f diff`** і **`patch -p1`** для авто-виправлень, потім повний прогін **`shellcheck`** по всіх tracked `*.sh` (у git) або по `**/*.sh` без `node_modules`. Потрібен також **`patch`** у `PATH` (на macOS зазвичай уже є).
|
|
117
|
+
|
|
115
118
|
У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` скрипт підставляє в v8r сам. За бажання можна передати власні glob-и аргументами скрипта. Шлях до скрипта: `./npm/scripts/…`, `./scripts/…` після копіювання, або `node_modules/@nitra/cursor/scripts/…`.
|
|
116
119
|
|
|
117
120
|
```json title="package.json"
|
|
118
121
|
{
|
|
119
122
|
"scripts": {
|
|
120
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
123
|
+
"lint-text": "npx cspell . && bun ./npm/scripts/run-shellcheck-text.mjs && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
121
124
|
},
|
|
122
125
|
"devDependencies": {
|
|
123
126
|
"@nitra/cspell-dict": "^2.1.0"
|
|
@@ -185,6 +188,7 @@ on:
|
|
|
185
188
|
- '**/*.go'
|
|
186
189
|
- '**/*.py'
|
|
187
190
|
- '**/*.php'
|
|
191
|
+
- '**/*.sh'
|
|
188
192
|
|
|
189
193
|
pull_request:
|
|
190
194
|
branches:
|
|
@@ -232,7 +236,7 @@ jobs:
|
|
|
232
236
|
```json title="package.json"
|
|
233
237
|
{
|
|
234
238
|
"scripts": {
|
|
235
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
239
|
+
"lint-text": "npx cspell . && bun ./npm/scripts/run-shellcheck-text.mjs && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
236
240
|
},
|
|
237
241
|
"devDependencies": {
|
|
238
242
|
"@nitra/cspell-dict": "^2.1.0"
|
|
@@ -249,7 +253,7 @@ jobs:
|
|
|
249
253
|
```json title="package.json"
|
|
250
254
|
{
|
|
251
255
|
"scripts": {
|
|
252
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
256
|
+
"lint-text": "npx cspell . && bun ./npm/scripts/run-shellcheck-text.mjs && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
253
257
|
},
|
|
254
258
|
"devDependencies": {
|
|
255
259
|
"@nitra/cspell-dict": "^2.1.0"
|
|
@@ -291,4 +295,4 @@ jobs:
|
|
|
291
295
|
|
|
292
296
|
## Перевірка
|
|
293
297
|
|
|
294
|
-
`npx @nitra/cursor check text` (охоплює oxfmt, cspell, markdownlint, v8r, CI для `lint-text`)
|
|
298
|
+
`npx @nitra/cursor check text` (охоплює oxfmt, cspell, shellcheck у `lint-text`, markdownlint, v8r, CI для `lint-text`)
|
package/package.json
CHANGED
package/scripts/check-text.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* дозволені лише **`@nitra/*`** (як у bun.mdc), зокрема **`@nitra/cspell-dict` ^2.0.0+**; без імпорту **`@cspell/dict-*`** у `.cspell.json`, заборона
|
|
11
11
|
* `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
|
|
12
12
|
* `.v8rignore` (vscode JSON),
|
|
13
|
-
* workflow `lint-text.yml`, розширення VSCode (markdownlint, oxc).
|
|
13
|
+
* workflow `lint-text.yml`, розширення VSCode (markdownlint, oxc, shellcheck), `run-shellcheck-text.mjs` у `lint-text`.
|
|
14
14
|
*
|
|
15
15
|
* Якщо є `.cursor/rules/n-text.mdc` і/або `npm/mdc/text.mdc` — перевіряє наявність абзацу про український
|
|
16
16
|
* апостроф (U+0027 vs U+2019) і приклад з символом U+2019 у тексті.
|
|
@@ -121,7 +121,7 @@ async function checkVscodeTextExtensions(passFn, failFn) {
|
|
|
121
121
|
try {
|
|
122
122
|
const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
123
123
|
const rec = ext.recommendations
|
|
124
|
-
for (const id of ['DavidAnson.vscode-markdownlint', 'oxc.oxc-vscode']) {
|
|
124
|
+
for (const id of ['DavidAnson.vscode-markdownlint', 'oxc.oxc-vscode', 'timonwong.shellcheck']) {
|
|
125
125
|
if (Array.isArray(rec) && rec.includes(id)) {
|
|
126
126
|
passFn(`extensions.json містить ${id}`)
|
|
127
127
|
} else {
|
|
@@ -329,15 +329,16 @@ function checkLintTextScript(lintText, passFn, failFn) {
|
|
|
329
329
|
const ok =
|
|
330
330
|
lt &&
|
|
331
331
|
lt.includes('cspell') &&
|
|
332
|
+
lt.includes('run-shellcheck-text.mjs') &&
|
|
332
333
|
lt.includes('bunx markdownlint-cli2') &&
|
|
333
334
|
lt.includes('**/*.mdc') &&
|
|
334
335
|
v8rTextOk &&
|
|
335
336
|
(!globsRequired || globsOk)
|
|
336
337
|
if (ok) {
|
|
337
|
-
passFn('package.json: lint-text — v8r: run-v8r.mjs
|
|
338
|
+
passFn('package.json: lint-text — shellcheck (run-shellcheck-text.mjs), v8r: run-v8r.mjs або чотири bunx v8r')
|
|
338
339
|
} else {
|
|
339
340
|
failFn(
|
|
340
|
-
'package.json: lint-text — v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ])
|
|
341
|
+
'package.json: lint-text — додай bun ./…/run-shellcheck-text.mjs; v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) (див. n-text.mdc)'
|
|
341
342
|
)
|
|
342
343
|
}
|
|
343
344
|
}
|
|
@@ -407,7 +408,7 @@ async function checkCspellConfig(pass, fail) {
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
/**
|
|
410
|
-
* Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, markdownlint через bunx, v8r)
|
|
411
|
+
* Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, shellcheck у lint-text, markdownlint через bunx, v8r)
|
|
411
412
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
412
413
|
*/
|
|
413
414
|
export async function check() {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запуск shellcheck у ланцюжку lint-text: спочатку авто-застосування виправлень, потім фінальна перевірка.
|
|
3
|
+
*
|
|
4
|
+
* ShellCheck не має прапорця «--fix»; для виправлень, які інструмент уміє запропонувати, використовується
|
|
5
|
+
* формат виводу `diff` і застосування патчу через `patch -p1` у корені проєкту (шляхи у unified diff від ShellCheck
|
|
6
|
+
* узгоджуються з цим режимом).
|
|
7
|
+
*
|
|
8
|
+
* Якщо `shellcheck` відсутній у PATH, скрипт завершується з кодом 1 і друкує підказки встановлення
|
|
9
|
+
* (macOS: Homebrew; Debian/Ubuntu: apt; Arch: pacman). Аналогічно для `patch`, якщо його немає
|
|
10
|
+
* (рідко на macOS/Linux).
|
|
11
|
+
*
|
|
12
|
+
* Список файлів: у git-робочому дереві — `git ls-files` з pathspec `:(glob)` для всіх tracked `*.sh`;
|
|
13
|
+
* інакше — `globSync` з виключенням `node_modules`. Якщо скриптів не знайдено — вихід 0.
|
|
14
|
+
*
|
|
15
|
+
* Після циклу авто-виправлень виконується звичайний `shellcheck` по всіх зібраних файлах; будь-яке
|
|
16
|
+
* попередження чи помилка — ненульовий код виходу.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import { globSync } from 'node:fs'
|
|
20
|
+
import { resolve } from 'node:path'
|
|
21
|
+
|
|
22
|
+
import { isRunAsCli } from './cli-entry.mjs'
|
|
23
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
24
|
+
|
|
25
|
+
/** Підрядок у stderr ShellCheck, коли є зауваження, але без авто-виправлення у форматі diff. */
|
|
26
|
+
const NON_AUTOFIXABLE_HINT = 'none were auto-fixable'
|
|
27
|
+
|
|
28
|
+
/** Максимум ітерацій `diff`+`patch` на один файл (захист від зациклення). */
|
|
29
|
+
const MAX_FIX_ROUNDS_PER_FILE = 32
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Друкує підказки встановлення shellcheck у stderr.
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
function printShellcheckInstallHints() {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
[
|
|
38
|
+
'❌ shellcheck не знайдено в PATH.',
|
|
39
|
+
'Встанови інструмент і повтори lint-text:',
|
|
40
|
+
' macOS: brew install shellcheck',
|
|
41
|
+
' Debian/Ubuntu: sudo apt-get install -y shellcheck',
|
|
42
|
+
' Arch: sudo pacman -S shellcheck',
|
|
43
|
+
''
|
|
44
|
+
].join('\n')
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Друкує підказку для відсутнього patch.
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
function printPatchInstallHints() {
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
[
|
|
55
|
+
'❌ patch не знайдено в PATH (потрібен для застосування diff від shellcheck).',
|
|
56
|
+
' macOS: patch зазвичай уже є; Debian/Ubuntu: sudo apt-get install -y patch',
|
|
57
|
+
''
|
|
58
|
+
].join('\n')
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Повертає відносні шляхи до shell-скриптів для перевірки.
|
|
64
|
+
* @param {string} cwd корінь проєкту
|
|
65
|
+
* @returns {string[]} відсортований масив шляхів відносно cwd
|
|
66
|
+
*/
|
|
67
|
+
export function listShellScriptPaths(cwd) {
|
|
68
|
+
const gitOk = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
69
|
+
cwd,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
env: process.env
|
|
72
|
+
})
|
|
73
|
+
if (gitOk.status === 0 && gitOk.stdout.trim() === 'true') {
|
|
74
|
+
const ls = spawnSync('git', ['ls-files', '-z', '--', ':(glob)**/*.sh'], {
|
|
75
|
+
cwd,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
env: process.env
|
|
78
|
+
})
|
|
79
|
+
if (ls.status !== 0) {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
const files = ls.stdout.split('\0').filter(Boolean)
|
|
83
|
+
return [...new Set(files)].sort()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fromGlob = globSync('**/*.sh', {
|
|
87
|
+
cwd,
|
|
88
|
+
exclude: p =>
|
|
89
|
+
p.includes('node_modules') ||
|
|
90
|
+
p.startsWith(`node_modules/`) ||
|
|
91
|
+
p.split('/').includes('node_modules')
|
|
92
|
+
})
|
|
93
|
+
return [...new Set(fromGlob.map(p => p.replaceAll('\\', '/')))].sort()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Запускає shellcheck із авто-виправленнями і фінальною перевіркою.
|
|
98
|
+
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
99
|
+
* @returns {number} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
|
|
100
|
+
*/
|
|
101
|
+
export function runShellcheckText(cwd = process.cwd()) {
|
|
102
|
+
const root = resolve(cwd)
|
|
103
|
+
const shellcheck = resolveCmd('shellcheck')
|
|
104
|
+
if (!shellcheck) {
|
|
105
|
+
printShellcheckInstallHints()
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
const patchBin = resolveCmd('patch')
|
|
109
|
+
if (!patchBin) {
|
|
110
|
+
printPatchInstallHints()
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = listShellScriptPaths(root)
|
|
115
|
+
if (files.length === 0) {
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const rel of files) {
|
|
120
|
+
for (let round = 0; round < MAX_FIX_ROUNDS_PER_FILE; round++) {
|
|
121
|
+
const diffResult = spawnSync(shellcheck, ['-f', 'diff', rel], {
|
|
122
|
+
cwd: root,
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
env: process.env,
|
|
125
|
+
maxBuffer: 10 * 1024 * 1024
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (diffResult.error) {
|
|
129
|
+
process.stderr.write(`${diffResult.error.message}\n`)
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const code = diffResult.status ?? 1
|
|
134
|
+
const out = (diffResult.stdout ?? '').trim()
|
|
135
|
+
const err = (diffResult.stderr ?? '').trim()
|
|
136
|
+
|
|
137
|
+
if (code === 0) {
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (err.includes(NON_AUTOFIXABLE_HINT) || !out) {
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const patchRun = spawnSync(patchBin, ['-p1'], {
|
|
146
|
+
cwd: root,
|
|
147
|
+
input: diffResult.stdout ?? '',
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
env: process.env
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (patchRun.status !== 0) {
|
|
153
|
+
if (patchRun.stderr?.length) {
|
|
154
|
+
process.stderr.write(patchRun.stderr)
|
|
155
|
+
}
|
|
156
|
+
if (patchRun.stdout?.length) {
|
|
157
|
+
process.stderr.write(patchRun.stdout)
|
|
158
|
+
}
|
|
159
|
+
process.stderr.write(`run-shellcheck-text: patch не застосував diff для ${rel}\n`)
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const finalRun = spawnSync(shellcheck, files, {
|
|
166
|
+
cwd: root,
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
env: process.env,
|
|
169
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
170
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (finalRun.error) {
|
|
174
|
+
process.stderr.write(`${finalRun.error.message}\n`)
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (finalRun.status !== 0) {
|
|
179
|
+
if (finalRun.stdout?.length) {
|
|
180
|
+
process.stdout.write(finalRun.stdout)
|
|
181
|
+
}
|
|
182
|
+
if (finalRun.stderr?.length) {
|
|
183
|
+
process.stderr.write(finalRun.stderr)
|
|
184
|
+
}
|
|
185
|
+
return 1
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isRunAsCli()) {
|
|
192
|
+
process.exitCode = runShellcheckText()
|
|
193
|
+
}
|