@nitra/cursor 1.13.34 → 1.13.40
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 +41 -1
- package/bin/n-cursor.js +4 -0
- package/package.json +1 -1
- package/rules/changelog/fix/consistency/check.mjs +100 -85
- package/rules/ci4/ci4.mdc +7 -7
- package/rules/ga/lint/lint.mjs +23 -3
- package/rules/ga/policy/lint_ga/lint_ga.rego +6 -0
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +6 -0
- package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/js-run/fix/runtime/check.mjs +3 -0
- package/rules/js-run/js-run.mdc +16 -1
- package/rules/js-run/policy/package_json/package_json.rego +17 -0
- package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
- package/rules/k8s/fix/manifests/check.mjs +775 -139
- package/rules/k8s/k8s.mdc +52 -6
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
- package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
- package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
- package/rules/security/fix/trufflehog/check.mjs +3 -0
- package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
- package/rules/text/fix/formatting/check.mjs +1 -1
- package/rules/text/lint/lint.mjs +113 -5
- package/rules/text/policy/cspell/cspell.rego +1 -1
- package/rules/text/policy/lint_text/lint_text.rego +100 -0
- package/rules/text/policy/lint_text/target.json +4 -0
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +61 -0
- package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
- package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
- package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/text/text.mdc +3 -57
- package/rules/vue/vue.mdc +1 -0
- package/scripts/sync-claude-config.mjs +2 -2
- package/scripts/utils/check-mdc-template-refs.mjs +15 -5
- package/scripts/utils/inline-template-links.mjs +15 -8
- package/scripts/utils/package-manifest.mjs +24 -19
- package/scripts/utils/run-conftest-batch.mjs +22 -15
- package/scripts/utils/template.mjs +89 -21
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Lint Text
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- '**/*.js'
|
|
10
|
+
- '**/*.ts'
|
|
11
|
+
- '**/*.vue'
|
|
12
|
+
- '**/*.html'
|
|
13
|
+
- '**/*.css'
|
|
14
|
+
- '**/*.scss'
|
|
15
|
+
- '**/*.less'
|
|
16
|
+
- '**/*.json'
|
|
17
|
+
- '**/*.jsonc'
|
|
18
|
+
- '**/*.yaml'
|
|
19
|
+
- '**/*.yml'
|
|
20
|
+
- '**/*.toml'
|
|
21
|
+
- '**/*.xml'
|
|
22
|
+
- '**/*.md'
|
|
23
|
+
- '**/*.mdc'
|
|
24
|
+
- '**/*.mdс'
|
|
25
|
+
- '**/*.txt'
|
|
26
|
+
- '**/*.go'
|
|
27
|
+
- '**/*.py'
|
|
28
|
+
- '**/*.php'
|
|
29
|
+
- '**/*.sh'
|
|
30
|
+
|
|
31
|
+
pull_request:
|
|
32
|
+
branches:
|
|
33
|
+
- dev
|
|
34
|
+
- main
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
38
|
+
cancel-in-progress: true
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
text:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: read
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v6
|
|
47
|
+
with:
|
|
48
|
+
persist-credentials: false
|
|
49
|
+
|
|
50
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
51
|
+
|
|
52
|
+
- name: Install shellcheck
|
|
53
|
+
run: sudo apt-get update && sudo apt-get install -y shellcheck
|
|
54
|
+
|
|
55
|
+
- name: Install dotenv-linter
|
|
56
|
+
run: >-
|
|
57
|
+
curl -sSfL https://git.io/JLbXn
|
|
58
|
+
| sh -s -- -b /usr/local/bin
|
|
59
|
+
|
|
60
|
+
- name: Lint text
|
|
61
|
+
run: bun run lint-text
|
|
@@ -47,7 +47,7 @@ deny contains msg if {
|
|
|
47
47
|
msg := sprintf(".markdownlint-cli2.jsonc: %s.%s.%s має бути %v (text.mdc)", [section, inner_key, leaf, expected])
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
# ── deny:
|
|
50
|
+
# ── deny: вкладеного обʼєкта взагалі немає ──────────────────────────────
|
|
51
51
|
|
|
52
52
|
deny contains msg if {
|
|
53
53
|
some section, expected_inner in data.template.snippet
|
package/rules/text/text.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.29'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
|
|
@@ -172,63 +172,9 @@ version: '1.28'
|
|
|
172
172
|
|
|
173
173
|
Додай workflow `.github/workflows/lint-text.yml`:
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
name: Lint Text
|
|
177
|
-
|
|
178
|
-
on:
|
|
179
|
-
push:
|
|
180
|
-
branches:
|
|
181
|
-
- dev
|
|
182
|
-
- main
|
|
183
|
-
paths:
|
|
184
|
-
- '**/*.js'
|
|
185
|
-
- '**/*.ts'
|
|
186
|
-
- '**/*.vue'
|
|
187
|
-
- '**/*.html'
|
|
188
|
-
- '**/*.css'
|
|
189
|
-
- '**/*.scss'
|
|
190
|
-
- '**/*.less'
|
|
191
|
-
- '**/*.json'
|
|
192
|
-
- '**/*.jsonc'
|
|
193
|
-
- '**/*.yaml'
|
|
194
|
-
- '**/*.yml'
|
|
195
|
-
- '**/*.toml'
|
|
196
|
-
- '**/*.xml'
|
|
197
|
-
- '**/*.md'
|
|
198
|
-
- '**/*.mdc'
|
|
199
|
-
- '**/*.mdс'
|
|
200
|
-
- '**/*.txt'
|
|
201
|
-
- '**/*.go'
|
|
202
|
-
- '**/*.py'
|
|
203
|
-
- '**/*.php'
|
|
204
|
-
- '**/*.sh'
|
|
205
|
-
|
|
206
|
-
pull_request:
|
|
207
|
-
branches:
|
|
208
|
-
- dev
|
|
209
|
-
- main
|
|
210
|
-
|
|
211
|
-
concurrency:
|
|
212
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
213
|
-
cancel-in-progress: true
|
|
214
|
-
|
|
215
|
-
jobs:
|
|
216
|
-
text:
|
|
217
|
-
runs-on: ubuntu-latest
|
|
218
|
-
permissions:
|
|
219
|
-
contents: read
|
|
220
|
-
steps:
|
|
221
|
-
- uses: actions/checkout@v6
|
|
222
|
-
with:
|
|
223
|
-
persist-credentials: false
|
|
224
|
-
|
|
225
|
-
- uses: ./.github/actions/setup-bun-deps
|
|
226
|
-
|
|
227
|
-
- name: Lint text
|
|
228
|
-
run: bun run lint-text
|
|
229
|
-
```
|
|
175
|
+
- Канон: [lint-text.yml.snippet.yml](./policy/lint_text/template/lint-text.yml.snippet.yml)
|
|
230
176
|
|
|
231
|
-
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
177
|
+
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Після composite — кроки **`Install shellcheck`** (apt) і **`Install dotenv-linter`** (curl), бо `n-cursor lint-text` вимагає обидва бінарники в PATH; на ubuntu-latest shellcheck часто вже є, dotenv-linter — ні. Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
232
178
|
|
|
233
179
|
Не дублюй окремий workflow з тими самими кроками cspell/markdownlint.
|
|
234
180
|
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -118,6 +118,7 @@ GlobalRegistrator.register()
|
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
`jsdom` не використовуй — happy-dom швидший і достатній для типових Vue-компонентних тестів.
|
|
121
|
+
|
|
121
122
|
- **E2E:** **Playwright** змістовні сценарії користувацьких потоків.
|
|
122
123
|
|
|
123
124
|
### Інструменти (узгоджено з Vite і цим правилом)
|
|
@@ -84,7 +84,7 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
|
|
|
84
84
|
/** Канонічний Cursor stop-hook для ADR capture. Cursor передає payload через stdin JSON. */
|
|
85
85
|
const CURSOR_ADR_STOP_HOOK = Object.freeze({
|
|
86
86
|
command: [
|
|
87
|
-
|
|
87
|
+
'bash -lc \'root="$PWD";',
|
|
88
88
|
`if [ ! -f "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
89
89
|
`bash "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}"'`
|
|
90
90
|
].join(' '),
|
|
@@ -94,7 +94,7 @@ const CURSOR_ADR_STOP_HOOK = Object.freeze({
|
|
|
94
94
|
/** Канонічний Cursor stop-hook для ADR normalize. */
|
|
95
95
|
const CURSOR_ADR_NORMALIZE_STOP_HOOK = Object.freeze({
|
|
96
96
|
command: [
|
|
97
|
-
|
|
97
|
+
'bash -lc \'root="$PWD";',
|
|
98
98
|
`if [ ! -f "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
99
99
|
`bash "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}"'`
|
|
100
100
|
].join(' '),
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns list of template/ files that are NOT referenced in <id>.mdc as
|
|
3
3
|
* markdown link targets. Paths returned are relative to ruleDir.
|
|
4
|
-
*
|
|
5
|
-
* @param {string} ruleDir absolute path to npm/rules/<id>/
|
|
6
|
-
* @param {string} ruleId basename (e.g. "security")
|
|
7
|
-
* @returns {Promise<string[]>}
|
|
8
4
|
*/
|
|
9
5
|
import { existsSync } from 'node:fs'
|
|
10
6
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
11
7
|
import { join, relative } from 'node:path'
|
|
12
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} ruleDir абсолютний шлях до каталогу правила
|
|
11
|
+
* @returns {Promise<string[]>} абсолютні шляхи всіх файлів у template/
|
|
12
|
+
*/
|
|
13
13
|
async function walkTemplateDirs(ruleDir) {
|
|
14
14
|
const out = []
|
|
15
15
|
for (const kind of ['fix', 'policy']) {
|
|
@@ -18,13 +18,18 @@ async function walkTemplateDirs(ruleDir) {
|
|
|
18
18
|
for (const concern of await readdir(kindDir)) {
|
|
19
19
|
const tpl = join(kindDir, concern, 'template')
|
|
20
20
|
if (!existsSync(tpl)) continue
|
|
21
|
-
|
|
21
|
+
const tplStat = await stat(tpl)
|
|
22
|
+
if (!tplStat.isDirectory()) continue
|
|
22
23
|
out.push(...(await collectFiles(tpl)))
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
return out.map(p => relative(ruleDir, p))
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} dir каталог для обходу
|
|
31
|
+
* @returns {Promise<string[]>} абсолютні шляхи знайдених файлів
|
|
32
|
+
*/
|
|
28
33
|
async function collectFiles(dir) {
|
|
29
34
|
const out = []
|
|
30
35
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
@@ -35,6 +40,11 @@ async function collectFiles(dir) {
|
|
|
35
40
|
return out
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} ruleDir абсолютний шлях до npm/rules/<id>/
|
|
45
|
+
* @param {string} ruleId basename правила (напр. "security")
|
|
46
|
+
* @returns {Promise<string[]>} відносні шляхи template-файлів без посилань у .mdc
|
|
47
|
+
*/
|
|
38
48
|
export async function findMissingMdcRefs(ruleDir, ruleId) {
|
|
39
49
|
const mdcPath = join(ruleDir, `${ruleId}.mdc`)
|
|
40
50
|
if (!existsSync(mdcPath)) return []
|
|
@@ -2,10 +2,14 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { basename, extname, join } from 'node:path'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
|
|
6
|
+
const TEMPLATE_SEGMENT_RE = /\/template\//
|
|
6
7
|
const SLOTS = ['snippet', 'deny', 'contains']
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} filePath шлях до файлу
|
|
11
|
+
* @returns {string} назва мови для fenced-блока
|
|
12
|
+
*/
|
|
9
13
|
function langFromExt(filePath) {
|
|
10
14
|
const ext = extname(filePath)
|
|
11
15
|
if (ext === '.json') return 'json'
|
|
@@ -16,10 +20,13 @@ function langFromExt(filePath) {
|
|
|
16
20
|
|
|
17
21
|
// Strip `.<slot>.<ext>` suffix (slot ∈ snippet/deny/contains) to recover the
|
|
18
22
|
// real target file name (e.g. `package.json.snippet.json` → `package.json`).
|
|
19
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} fileBasename базове ім'я template-файлу
|
|
25
|
+
* @returns {string} ім'я реального target-файлу
|
|
26
|
+
*/
|
|
20
27
|
function normalizeTargetName(fileBasename) {
|
|
21
28
|
for (const slot of SLOTS) {
|
|
22
|
-
const m = fileBasename.match(new RegExp(`^(.+)
|
|
29
|
+
const m = fileBasename.match(new RegExp(String.raw`^(.+)\.${slot}\.[^.]+$`))
|
|
23
30
|
if (m) return m[1]
|
|
24
31
|
}
|
|
25
32
|
return fileBasename
|
|
@@ -29,19 +36,18 @@ function normalizeTargetName(fileBasename) {
|
|
|
29
36
|
* Finds markdown links whose path contains /template/ and replaces them with
|
|
30
37
|
* inline fenced blocks. Reads file from join(ruleDir, rel-path).
|
|
31
38
|
* Throws Error if a matched link target doesn't exist (fail loud — user must know).
|
|
32
|
-
*
|
|
33
39
|
* @param {string} text .mdc file contents
|
|
34
40
|
* @param {string} ruleDir absolute path to the rule directory (e.g. .../npm/rules/security/)
|
|
35
41
|
* @returns {Promise<string>} transformed text
|
|
36
42
|
*/
|
|
37
43
|
export async function inlineTemplateLinks(text, ruleDir) {
|
|
38
|
-
const matches = [...text.matchAll(
|
|
44
|
+
const matches = [...text.matchAll(MD_LINK_RE)].filter(m => TEMPLATE_SEGMENT_RE.test(m[2]))
|
|
39
45
|
if (matches.length === 0) return text
|
|
40
46
|
|
|
41
47
|
let result = text
|
|
42
48
|
for (const match of matches) {
|
|
43
49
|
const [fullMatch, , href] = match
|
|
44
|
-
// href starts with ./ and contains /template/
|
|
50
|
+
// href starts with ./ (regex) and contains /template/ (filter above)
|
|
45
51
|
const relPath = href.slice(2) // strip leading ./
|
|
46
52
|
const absPath = join(ruleDir, relPath)
|
|
47
53
|
|
|
@@ -49,7 +55,8 @@ export async function inlineTemplateLinks(text, ruleDir) {
|
|
|
49
55
|
throw new Error(`inlineTemplateLinks: file not found: ${absPath} (referenced from .mdc)`)
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
const
|
|
58
|
+
const raw = await readFile(absPath, 'utf8')
|
|
59
|
+
const contents = raw.trim()
|
|
53
60
|
const lang = langFromExt(absPath)
|
|
54
61
|
const targetName = normalizeTargetName(basename(absPath))
|
|
55
62
|
const replacement = `\`${targetName}\`:\n\n\`\`\`${lang}\n${contents}\n\`\`\``
|
|
@@ -10,16 +10,24 @@ import { parse as parseToml } from 'smol-toml'
|
|
|
10
10
|
|
|
11
11
|
import { getMonorepoPackageRootDirs } from './workspaces.mjs'
|
|
12
12
|
|
|
13
|
-
/** @typedef {'npm' | 'python'} PackageKind */
|
|
14
|
-
|
|
15
13
|
/**
|
|
14
|
+
@typedef {'npm' | 'python'} PackageKind
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/*
|
|
16
18
|
* @typedef {object} PackageManifest
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
* @property {PackageKind} kind поле
|
|
18
21
|
* @property {string} ws відносний шлях воркспейсу (`'.'` для кореня)
|
|
22
|
+
|
|
19
23
|
* @property {string} manifestRel `package.json` | `pyproject.toml`
|
|
24
|
+
|
|
20
25
|
* @property {string | null} name ім'я пакета (npm / PyPI)
|
|
26
|
+
|
|
21
27
|
* @property {string | null} version semver-рядок
|
|
28
|
+
|
|
22
29
|
* @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
|
|
30
|
+
|
|
23
31
|
* @property {string[] | null} [npmFiles] лише npm: `files` з package.json
|
|
24
32
|
*/
|
|
25
33
|
|
|
@@ -27,7 +35,7 @@ const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**'
|
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
37
|
* @param {unknown} doc розпарсений pyproject.toml
|
|
30
|
-
* @returns {{ name: string | null, version: string | null }}
|
|
38
|
+
* @returns {{ name: string | null, version: string | null }} витягнуті поля project / tool.poetry
|
|
31
39
|
*/
|
|
32
40
|
function projectFieldsFromPyprojectDoc(doc) {
|
|
33
41
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
@@ -39,7 +47,7 @@ function projectFieldsFromPyprojectDoc(doc) {
|
|
|
39
47
|
const p = /** @type {Record<string, unknown>} */ (project)
|
|
40
48
|
return {
|
|
41
49
|
name: typeof p.name === 'string' ? p.name : null,
|
|
42
|
-
version: typeof p.version === 'string' ? p.version : null
|
|
50
|
+
version: typeof p.version === 'string' ? p.version : null
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
const tool = root.tool
|
|
@@ -49,7 +57,7 @@ function projectFieldsFromPyprojectDoc(doc) {
|
|
|
49
57
|
const po = /** @type {Record<string, unknown>} */ (poetry)
|
|
50
58
|
return {
|
|
51
59
|
name: typeof po.name === 'string' ? po.name : null,
|
|
52
|
-
version: typeof po.version === 'string' ? po.version : null
|
|
60
|
+
version: typeof po.version === 'string' ? po.version : null
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
}
|
|
@@ -58,7 +66,7 @@ function projectFieldsFromPyprojectDoc(doc) {
|
|
|
58
66
|
|
|
59
67
|
/**
|
|
60
68
|
* @param {string} text вміст pyproject.toml
|
|
61
|
-
* @returns {{ name: string | null, version: string | null }}
|
|
69
|
+
* @returns {{ name: string | null, version: string | null }} витягнуті поля project / tool.poetry
|
|
62
70
|
*/
|
|
63
71
|
export function parsePyprojectFields(text) {
|
|
64
72
|
try {
|
|
@@ -70,7 +78,7 @@ export function parsePyprojectFields(text) {
|
|
|
70
78
|
|
|
71
79
|
/**
|
|
72
80
|
* @param {string} ws шлях воркспейсу
|
|
73
|
-
* @returns {Promise<PackageManifest | null>}
|
|
81
|
+
* @returns {Promise<PackageManifest | null>} маніфест пакета або null
|
|
74
82
|
*/
|
|
75
83
|
export async function readPackageManifest(ws) {
|
|
76
84
|
const pkgPath = join(ws, 'package.json')
|
|
@@ -82,10 +90,7 @@ export async function readPackageManifest(ws) {
|
|
|
82
90
|
}
|
|
83
91
|
const pkg = /** @type {Record<string, unknown>} */ (parsed)
|
|
84
92
|
const registryPublishable =
|
|
85
|
-
typeof pkg.name === 'string' &&
|
|
86
|
-
pkg.name.length > 0 &&
|
|
87
|
-
pkg.private !== true &&
|
|
88
|
-
Array.isArray(pkg.files)
|
|
93
|
+
typeof pkg.name === 'string' && pkg.name.length > 0 && pkg.private !== true && Array.isArray(pkg.files)
|
|
89
94
|
return {
|
|
90
95
|
kind: 'npm',
|
|
91
96
|
ws,
|
|
@@ -93,7 +98,7 @@ export async function readPackageManifest(ws) {
|
|
|
93
98
|
name: typeof pkg.name === 'string' ? pkg.name : null,
|
|
94
99
|
version: typeof pkg.version === 'string' ? pkg.version : null,
|
|
95
100
|
registryPublishable,
|
|
96
|
-
npmFiles: Array.isArray(pkg.files) ? pkg.files : null
|
|
101
|
+
npmFiles: Array.isArray(pkg.files) ? pkg.files : null
|
|
97
102
|
}
|
|
98
103
|
} catch {
|
|
99
104
|
return null
|
|
@@ -113,14 +118,14 @@ export async function readPackageManifest(ws) {
|
|
|
113
118
|
name: fields.name,
|
|
114
119
|
version: fields.version,
|
|
115
120
|
registryPublishable,
|
|
116
|
-
npmFiles: null
|
|
121
|
+
npmFiles: null
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/**
|
|
121
126
|
* Каталоги пакетів: npm (`package.json` / workspaces) + Python (`pyproject.toml` без package.json).
|
|
122
|
-
* @param {string} [repoRoot]
|
|
123
|
-
* @returns {Promise<string[]>}
|
|
127
|
+
* @param {string} [repoRoot] параметр
|
|
128
|
+
* @returns {Promise<string[]>} результат
|
|
124
129
|
*/
|
|
125
130
|
export async function getMonorepoProjectRootDirs(repoRoot = '.') {
|
|
126
131
|
const roots = new Set(await getMonorepoPackageRootDirs(repoRoot))
|
|
@@ -149,9 +154,9 @@ export async function getMonorepoProjectRootDirs(repoRoot = '.') {
|
|
|
149
154
|
|
|
150
155
|
/**
|
|
151
156
|
* Шлях до файлу маніфесту воркспейсу.
|
|
152
|
-
* @param {string} ws
|
|
153
|
-
* @param {PackageManifest} manifest
|
|
154
|
-
* @returns {string}
|
|
157
|
+
* @param {string} ws параметр
|
|
158
|
+
* @param {PackageManifest} manifest параметр
|
|
159
|
+
* @returns {string} результат
|
|
155
160
|
*/
|
|
156
161
|
export function manifestFilePath(ws, manifest) {
|
|
157
162
|
return join(ws, manifest.manifestRel)
|
|
@@ -22,7 +22,9 @@ import { fileURLToPath } from 'node:url'
|
|
|
22
22
|
|
|
23
23
|
import { resolveCmd } from './resolve-cmd.mjs'
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
Каталог пакета `@nitra/cursor`, від якого ресолвимо вшиті директорії правил.
|
|
27
|
+
*/
|
|
26
28
|
const PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
|
|
27
29
|
|
|
28
30
|
/** Шлях до кореня правил. У npm-tarball публікується через `files: ["rules"]`. Кожне правило: `rules/<id>/policy/<name>/`. */
|
|
@@ -45,19 +47,27 @@ function failConftestMissing() {
|
|
|
45
47
|
)
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
/*
|
|
49
51
|
* @typedef {object} ConftestViolation
|
|
52
|
+
|
|
50
53
|
* @property {string} filename абсолютний шлях до файла, що дав порушення (з output conftest)
|
|
54
|
+
|
|
51
55
|
* @property {string} message текст порушення (як у `deny` rego-пакета)
|
|
56
|
+
|
|
52
57
|
* @property {string} namespace namespace rego-пакета (наприклад `abie.base_deployment_preem`)
|
|
53
58
|
*/
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
/*
|
|
56
61
|
* @typedef {object} ConftestBatchOptions
|
|
62
|
+
|
|
57
63
|
* @property {string} policyDirRel шлях до підкаталогу `npm/policy/...` (наприклад `abie/base_deployment_preem`)
|
|
64
|
+
|
|
58
65
|
* @property {string} namespace повне імʼя rego-пакета (наприклад `abie.base_deployment_preem`)
|
|
66
|
+
|
|
59
67
|
* @property {string[]} files список абсолютних шляхів файлів для перевірки (порожній — повертаємо порожньо)
|
|
68
|
+
|
|
60
69
|
* @property {string[]} [extraArgs] додаткові аргументи для conftest (наприклад `--combine` для крос-документних правил)
|
|
70
|
+
|
|
61
71
|
* @property {object} [templateData] опціональне merged-дерево; серіалізується у JSON `{ "template": <data> }` і передається як `--data <tmpfile>` (cleanup після завершення)
|
|
62
72
|
*/
|
|
63
73
|
|
|
@@ -65,18 +75,11 @@ function failConftestMissing() {
|
|
|
65
75
|
* Pure args builder for conftest test. Extracted for unit-testability.
|
|
66
76
|
* Preserves the existing args layout (files before -p; --output json --no-color
|
|
67
77
|
* for parseable output); inserts --data right after --namespace when provided.
|
|
68
|
-
* @param {{ policyAbs: string, namespace: string, files: string[], extraArgs: string[], tmpDataFile: string|null }} p
|
|
69
|
-
* @returns {string[]}
|
|
78
|
+
* @param {{ policyAbs: string, namespace: string, files: string[], extraArgs: string[], tmpDataFile: string|null }} p параметри батчу
|
|
79
|
+
* @returns {string[]} args для виклику conftest
|
|
70
80
|
*/
|
|
71
81
|
export function buildConftestArgs(p) {
|
|
72
|
-
const args = [
|
|
73
|
-
'test',
|
|
74
|
-
...p.files,
|
|
75
|
-
'-p',
|
|
76
|
-
p.policyAbs,
|
|
77
|
-
'--namespace',
|
|
78
|
-
p.namespace
|
|
79
|
-
]
|
|
82
|
+
const args = ['test', ...p.files, '-p', p.policyAbs, '--namespace', p.namespace]
|
|
80
83
|
if (p.tmpDataFile) args.push('--data', p.tmpDataFile)
|
|
81
84
|
args.push('--output', 'json', '--no-color', ...p.extraArgs)
|
|
82
85
|
return args
|
|
@@ -125,14 +128,18 @@ export function runConftestBatch(opts) {
|
|
|
125
128
|
if (result.status !== 0 && result.status !== 1) {
|
|
126
129
|
throw new Error(`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
|
|
127
130
|
}
|
|
128
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
@type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>}
|
|
133
|
+
*/
|
|
129
134
|
let parsed
|
|
130
135
|
try {
|
|
131
136
|
parsed = JSON.parse(result.stdout)
|
|
132
137
|
} catch {
|
|
133
138
|
throw new Error(`conftest stdout не парситься як JSON: ${(result.stdout || '').slice(0, 200)}`)
|
|
134
139
|
}
|
|
135
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
@type {ConftestViolation[]}
|
|
142
|
+
*/
|
|
136
143
|
const out = []
|
|
137
144
|
for (const entry of parsed) {
|
|
138
145
|
const failures = entry.failures ?? []
|