@nitra/cursor 1.8.189 → 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 +28 -0
- package/mdc/js-run.mdc +21 -2
- package/mdc/npm-module.mdc +8 -2
- package/mdc/text.mdc +11 -7
- package/package.json +1 -1
- package/scripts/check-js-run.mjs +99 -2
- package/scripts/check-npm-module.mjs +130 -0
- package/scripts/check-text.mjs +6 -5
- package/scripts/run-shellcheck-text.mjs +193 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
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
|
+
|
|
18
|
+
## [1.8.191] - 2026-05-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `check-npm-module.mjs`: перший заголовок **`## [version]`** у `npm/CHANGELOG.md` має збігатися з **`version`** у `npm/package.json` (найсвіжіший реліз зверху — Keep a Changelog); якщо є незакомічені зміни під **`npm/`**, `version` у робочому `npm/package.json` має відрізнятися від **`HEAD`** (інакше ризик дописати новий функціонал без bump).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `npm-module` (mdc v1.9 → v1.10): розширено **«Build версія»** і **«CHANGELOG»** — чеклист для агента, заборона дописувати нові пункти в уже існуючу секцію релізу замість нового номера; впорядковано `CHANGELOG` (1.8.190 перед 1.8.189).
|
|
27
|
+
|
|
28
|
+
## [1.8.190] - 2026-05-07
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `js-run` (mdc v1.4 → v1.5): секція **`jsconfig.json`** — канонічний файл для backend-пакетів із каталогом **`src/`** (NodeNext, `include: ['src/**/*']`); для пакетів без `src/` вимога не діє.
|
|
33
|
+
- `check-js-run.mjs`: перевірка наявності та вмісту `jsconfig.json`, якщо в workspace-пакеті (без vite) є **`src/`**; тести у `check-js-run-fixture.test.mjs`.
|
|
34
|
+
|
|
7
35
|
## [1.8.189] - 2026-05-07
|
|
8
36
|
|
|
9
37
|
### 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
|
+
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`** і збіг вмісту з каноном вище.
|
package/mdc/npm-module.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
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/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-js-run.mjs
CHANGED
|
@@ -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
|
}
|
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
|
+
}
|