@nitra/cursor 1.8.204 → 1.8.207
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 +52 -1
- package/bin/auto-rules.md +2 -0
- package/mdc/rego.mdc +77 -0
- package/package.json +1 -1
- package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
- package/policy/abie/http_route_base/http_route_base.rego +45 -0
- package/policy/adr/settings_json/settings_json.rego +31 -0
- package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
- package/policy/bun/bunfig/bunfig.rego +33 -0
- package/policy/bun/package_json/package_json.rego +94 -0
- package/policy/capacitor/package_json/package_json.rego +45 -0
- package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
- package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
- package/policy/ga/git_ai/git_ai.rego +83 -0
- package/policy/ga/lint_ga/lint_ga.rego +118 -0
- package/policy/ga/workflow_common/workflow_common.rego +161 -0
- package/policy/graphql/package_json/package_json.rego +35 -0
- package/policy/hasura/svc_hl/svc_hl.rego +27 -0
- package/policy/image_compress/package_json/package_json.rego +94 -0
- package/policy/js_bun_db/package_json/package_json.rego +28 -0
- package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
- package/policy/js_lint/package_json/package_json.rego +137 -0
- package/policy/js_mssql/package_json/package_json.rego +57 -0
- package/policy/js_run/configmap/configmap.rego +45 -0
- package/policy/js_run/jsconfig/jsconfig.rego +66 -0
- package/policy/js_run/package_json/package_json.rego +31 -0
- package/policy/k8s/manifest/manifest.rego +130 -0
- package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
- package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
- package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
- package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
- package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
- package/policy/php/package_json/package_json.rego +19 -0
- package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
- package/policy/style_lint/package_json/package_json.rego +49 -0
- package/policy/text/cspell/cspell.rego +91 -0
- package/policy/text/markdownlint/markdownlint.rego +21 -0
- package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
- package/policy/text/package_json/package_json.rego +88 -0
- package/policy/vue/package_json/package_json.rego +54 -0
- package/scripts/auto-rules.mjs +10 -0
- package/scripts/check-adr.mjs +7 -3
- package/scripts/check-bun.mjs +21 -117
- package/scripts/check-ga.mjs +0 -284
- package/scripts/check-graphql.mjs +6 -45
- package/scripts/check-hasura.mjs +4 -5
- package/scripts/check-image-avif.mjs +3 -3
- package/scripts/check-image-compress.mjs +25 -132
- package/scripts/check-js-bun-db.mjs +3 -50
- package/scripts/check-js-run.mjs +9 -12
- package/scripts/check-k8s.mjs +6 -5
- package/scripts/check-npm-module.mjs +17 -8
- package/scripts/check-php.mjs +16 -51
- package/scripts/check-style-lint.mjs +28 -52
- package/scripts/check-text.mjs +47 -219
- package/scripts/check-vue.mjs +3 -16
- package/scripts/lint-conftest.mjs +351 -0
- package/scripts/lint-ga.mjs +49 -2
- package/scripts/lint-rego.mjs +67 -21
- package/scripts/run-shellcheck-text.mjs +3 -6
- package/scripts/utils/depcheck-workflow.mjs +2 -6
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Прогоняє `conftest test` по всіх Rego-полісі з `npm/policy/` (окрім `ga/*`,
|
|
3
|
+
* які вже виконуються через `lint-ga.mjs`).
|
|
4
|
+
*
|
|
5
|
+
* Кожна полісі має свій namespace, опційний `rule` (id у `.n-cursor.json:rules`,
|
|
6
|
+
* інакше таргет пропускається — як гейтинг у `check-*.mjs`), і список цільових
|
|
7
|
+
* файлів — single-file або walk-предикат для дерева. Якщо цільових файлів немає
|
|
8
|
+
* або правило не активне — таргет мовчки пропускається.
|
|
9
|
+
*
|
|
10
|
+
* Поведінка fallback:
|
|
11
|
+
* - якщо `conftest` не в `PATH` — друкуємо `ℹ` повідомлення з підказкою
|
|
12
|
+
* встановлення і повертаємо 0 (структурні JS-перевірки в `check-*.mjs`
|
|
13
|
+
* лишаються паралельно). Те саме рішення — у `lint-ga.mjs`.
|
|
14
|
+
* - якщо `npm/policy/` не існує (нетипова інсталяція) — також `ℹ` skip.
|
|
15
|
+
*
|
|
16
|
+
* Перший ненульовий exit-код conftest — повертаємо як результат, але всі
|
|
17
|
+
* наступні таргети все одно виконуємо, щоб одразу побачити повний список
|
|
18
|
+
* порушень (а не виправляти по одному).
|
|
19
|
+
*
|
|
20
|
+
* Експортовано окремо `runLintConftestCli` — використовується з
|
|
21
|
+
* `bin/n-cursor.js` як підкоманда `lint-conftest`.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
24
|
+
import { spawnSync } from 'node:child_process'
|
|
25
|
+
import { dirname, join, sep } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
|
|
28
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
29
|
+
|
|
30
|
+
/** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиту директорію policy/. */
|
|
31
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
32
|
+
|
|
33
|
+
/** Шлях до кореня Rego-полісі. У npm-tarball публікується через `files: ["policy"]`. */
|
|
34
|
+
const POLICY_DIR = join(PACKAGE_ROOT, 'policy')
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Опис одного таргета: namespace + спосіб розвʼязати цільові файли.
|
|
38
|
+
*
|
|
39
|
+
* `single` — конкретний файл відносно cwd, перевіряється `existsSync`-ом.
|
|
40
|
+
* `walk` — рекурсивний обхід від cwd із простим суфікс-предикатом
|
|
41
|
+
* (наприклад `name === 'package.json'`). Глибокі ігнори — як у `walkDir`
|
|
42
|
+
* в інших скриптах: `node_modules`, `.git`, `dist`, `coverage`, `build`,
|
|
43
|
+
* `.turbo`, `.next`. Не використовуємо bun Glob, щоб не плодити залежності
|
|
44
|
+
* за межами `node:fs`.
|
|
45
|
+
*
|
|
46
|
+
* @typedef {{
|
|
47
|
+
* namespace: string,
|
|
48
|
+
* policyDir: string,
|
|
49
|
+
* rule?: string,
|
|
50
|
+
* single?: string,
|
|
51
|
+
* walk?: { match: (relPosix: string) => boolean }
|
|
52
|
+
* }} ConftestTarget
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Зчитує `rules` з `.n-cursor.json` у cwd. Повертає множину рядків — або `null`,
|
|
57
|
+
* якщо файлу немає чи поле некоректне (тоді гейтинг вимикаємо — як у `check-bun.mjs`).
|
|
58
|
+
* @param {string} cwd корінь репо
|
|
59
|
+
* @returns {Set<string> | null} множина активних правил або null
|
|
60
|
+
*/
|
|
61
|
+
function loadActiveCursorRules(cwd) {
|
|
62
|
+
const path = join(cwd, '.n-cursor.json')
|
|
63
|
+
if (!existsSync(path)) return null
|
|
64
|
+
try {
|
|
65
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'))
|
|
66
|
+
if (!Array.isArray(raw?.rules)) return null
|
|
67
|
+
return new Set(raw.rules.map(String))
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SKIP_DIR_NAMES = new Set(['node_modules', '.git', 'dist', 'coverage', 'build', '.turbo', '.next'])
|
|
74
|
+
|
|
75
|
+
/** @type {ConftestTarget[]} */
|
|
76
|
+
const TARGETS = [
|
|
77
|
+
// ── bun ─────────────────────────────────────────────────────────────────
|
|
78
|
+
{ namespace: 'bun.bunfig', policyDir: 'bun', rule: 'bun', single: 'bunfig.toml' },
|
|
79
|
+
{ namespace: 'bun.package_json', policyDir: 'bun', rule: 'bun', single: 'package.json' },
|
|
80
|
+
|
|
81
|
+
// ── text ────────────────────────────────────────────────────────────────
|
|
82
|
+
{ namespace: 'text.oxfmtrc', policyDir: 'text', rule: 'text', single: '.oxfmtrc.json' },
|
|
83
|
+
{ namespace: 'text.cspell', policyDir: 'text', rule: 'text', single: '.cspell.json' },
|
|
84
|
+
{ namespace: 'text.markdownlint', policyDir: 'text', rule: 'text', single: '.markdownlint-cli2.jsonc' },
|
|
85
|
+
{ namespace: 'text.package_json', policyDir: 'text', rule: 'text', single: 'package.json' },
|
|
86
|
+
|
|
87
|
+
// ── style-lint ──────────────────────────────────────────────────────────
|
|
88
|
+
{ namespace: 'style_lint.package_json', policyDir: 'style_lint', rule: 'style-lint', single: 'package.json' },
|
|
89
|
+
{
|
|
90
|
+
namespace: 'style_lint.lint_style_yml',
|
|
91
|
+
policyDir: 'style_lint',
|
|
92
|
+
rule: 'style-lint',
|
|
93
|
+
single: '.github/workflows/lint-style.yml'
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// ── php ─────────────────────────────────────────────────────────────────
|
|
97
|
+
{ namespace: 'php.package_json', policyDir: 'php', rule: 'php', single: 'package.json' },
|
|
98
|
+
{
|
|
99
|
+
namespace: 'php.lint_php_yml',
|
|
100
|
+
policyDir: 'php',
|
|
101
|
+
rule: 'php',
|
|
102
|
+
single: '.github/workflows/lint-php.yml'
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// ── npm-module ──────────────────────────────────────────────────────────
|
|
106
|
+
{
|
|
107
|
+
namespace: 'npm_module.root_package_json',
|
|
108
|
+
policyDir: 'npm_module',
|
|
109
|
+
rule: 'npm-module',
|
|
110
|
+
single: 'package.json'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
namespace: 'npm_module.npm_package_json',
|
|
114
|
+
policyDir: 'npm_module',
|
|
115
|
+
rule: 'npm-module',
|
|
116
|
+
single: 'npm/package.json'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
namespace: 'npm_module.emit_types_config',
|
|
120
|
+
policyDir: 'npm_module',
|
|
121
|
+
rule: 'npm-module',
|
|
122
|
+
single: 'npm/tsconfig.emit-types.json'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
namespace: 'npm_module.npm_publish_yml',
|
|
126
|
+
policyDir: 'npm_module',
|
|
127
|
+
rule: 'npm-module',
|
|
128
|
+
single: '.github/workflows/npm-publish.yml'
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// ── js-lint ─────────────────────────────────────────────────────────────
|
|
132
|
+
{ namespace: 'js_lint.package_json', policyDir: 'js_lint', rule: 'js-lint', single: 'package.json' },
|
|
133
|
+
{
|
|
134
|
+
namespace: 'js_lint.lint_js_yml',
|
|
135
|
+
policyDir: 'js_lint',
|
|
136
|
+
rule: 'js-lint',
|
|
137
|
+
single: '.github/workflows/lint-js.yml'
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// ── graphql / image-compress / capacitor ────────────────────────────────
|
|
141
|
+
{ namespace: 'graphql.package_json', policyDir: 'graphql', rule: 'graphql', single: 'package.json' },
|
|
142
|
+
{
|
|
143
|
+
namespace: 'image_compress.package_json',
|
|
144
|
+
policyDir: 'image_compress',
|
|
145
|
+
rule: 'image-compress',
|
|
146
|
+
single: 'package.json'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
namespace: 'capacitor.package_json',
|
|
150
|
+
policyDir: 'capacitor',
|
|
151
|
+
rule: 'capacitor',
|
|
152
|
+
single: 'package.json'
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ── hasura ──────────────────────────────────────────────────────────────
|
|
156
|
+
{
|
|
157
|
+
namespace: 'hasura.svc_hl',
|
|
158
|
+
policyDir: 'hasura',
|
|
159
|
+
rule: 'hasura',
|
|
160
|
+
single: 'hasura/k8s/base/svc-hl.yaml'
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// ── adr ─────────────────────────────────────────────────────────────────
|
|
164
|
+
{ namespace: 'adr.settings_json', policyDir: 'adr', rule: 'adr', single: '.claude/settings.json' },
|
|
165
|
+
{
|
|
166
|
+
namespace: 'adr.settings_local_json',
|
|
167
|
+
policyDir: 'adr',
|
|
168
|
+
rule: 'adr',
|
|
169
|
+
single: '.claude/settings.local.json'
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ── multi-file (walk) ───────────────────────────────────────────────────
|
|
173
|
+
// Усі `package.json` у дереві (включно з workspace-пакетами).
|
|
174
|
+
{
|
|
175
|
+
namespace: 'js_mssql.package_json',
|
|
176
|
+
policyDir: 'js_mssql',
|
|
177
|
+
rule: 'js-mssql',
|
|
178
|
+
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
namespace: 'js_bun_db.package_json',
|
|
182
|
+
policyDir: 'js_bun_db',
|
|
183
|
+
rule: 'js-bun-db',
|
|
184
|
+
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
namespace: 'js_run.package_json',
|
|
188
|
+
policyDir: 'js_run',
|
|
189
|
+
rule: 'js-run',
|
|
190
|
+
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
namespace: 'vue.package_json',
|
|
194
|
+
policyDir: 'vue',
|
|
195
|
+
rule: 'vue',
|
|
196
|
+
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// ConfigMap у `…/k8s/base/configmap.yaml` будь-де у дереві.
|
|
200
|
+
{
|
|
201
|
+
namespace: 'js_run.configmap',
|
|
202
|
+
policyDir: 'js_run',
|
|
203
|
+
rule: 'js-run',
|
|
204
|
+
walk: { match: rel => /(^|\/)k8s\/[^/]+\/configmap\.yaml$/.test(rel) }
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Усі YAML у дереві з сегментом `k8s` — пер-документні структурні правила.
|
|
208
|
+
{
|
|
209
|
+
namespace: 'k8s.manifest',
|
|
210
|
+
policyDir: 'k8s',
|
|
211
|
+
rule: 'k8s',
|
|
212
|
+
walk: { match: rel => /(^|\/)k8s\//.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// abie HealthCheckPolicy: `hc.yaml` у дереві k8s.
|
|
216
|
+
{
|
|
217
|
+
namespace: 'abie.health_check_policy',
|
|
218
|
+
policyDir: 'abie',
|
|
219
|
+
rule: 'abie',
|
|
220
|
+
walk: { match: rel => /(^|\/)k8s\/.+\/hc\.yaml$/.test(rel) }
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// abie HTTPRoute у `base/`.
|
|
224
|
+
{
|
|
225
|
+
namespace: 'abie.http_route_base',
|
|
226
|
+
policyDir: 'abie',
|
|
227
|
+
rule: 'abie',
|
|
228
|
+
walk: { match: rel => /(^|\/)k8s\/.*base\/.*hr\.yaml$/.test(rel) }
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Рекурсивно збирає відносні (posix) шляхи від cwd, які матчаться предикатом.
|
|
234
|
+
* Глибокі ігнори — `SKIP_DIR_NAMES`. Не йде у симлінки, помилки stat — мовчки skip.
|
|
235
|
+
* @param {string} root абсолютний корінь обходу
|
|
236
|
+
* @param {(relPosix: string) => boolean} match предикат на відносний posix-шлях
|
|
237
|
+
* @returns {string[]} список відносних posix-шляхів
|
|
238
|
+
*/
|
|
239
|
+
function collectFiles(root, match) {
|
|
240
|
+
/** @type {string[]} */
|
|
241
|
+
const out = []
|
|
242
|
+
/** @param {string} dirAbs */
|
|
243
|
+
function visit(dirAbs) {
|
|
244
|
+
/** @type {import('node:fs').Dirent[]} */
|
|
245
|
+
let entries
|
|
246
|
+
try {
|
|
247
|
+
entries = readdirSync(dirAbs, { withFileTypes: true })
|
|
248
|
+
} catch {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
for (const e of entries) {
|
|
252
|
+
if (e.isSymbolicLink()) continue
|
|
253
|
+
const abs = join(dirAbs, e.name)
|
|
254
|
+
if (e.isDirectory()) {
|
|
255
|
+
if (SKIP_DIR_NAMES.has(e.name)) continue
|
|
256
|
+
visit(abs)
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
if (!e.isFile()) continue
|
|
260
|
+
const rel = abs.slice(root.length + 1).split(sep).join('/')
|
|
261
|
+
if (match(rel)) out.push(rel)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
visit(root)
|
|
265
|
+
return out
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Розвʼязує файлові цілі для одного таргета щодо cwd.
|
|
270
|
+
* @param {ConftestTarget} target опис таргета
|
|
271
|
+
* @param {string} cwd корінь репозиторію
|
|
272
|
+
* @returns {string[]} список абсолютних / відносних шляхів
|
|
273
|
+
*/
|
|
274
|
+
function resolveTargetFiles(target, cwd) {
|
|
275
|
+
if (target.single) {
|
|
276
|
+
return existsSync(join(cwd, target.single)) ? [target.single] : []
|
|
277
|
+
}
|
|
278
|
+
if (target.walk) {
|
|
279
|
+
return collectFiles(cwd, target.walk.match)
|
|
280
|
+
}
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Запускає conftest на одному таргеті. Повертає exit-код (0 — OK, 1+ — помилки).
|
|
286
|
+
*
|
|
287
|
+
* При відсутніх цільових файлах — мовчки повертає 0 (правило неактуальне для repo).
|
|
288
|
+
* Логує заголовок з namespace і кількістю файлів, як `lint-ga.mjs`.
|
|
289
|
+
* @param {string} conftestBin абсолютний шлях до бінарника conftest
|
|
290
|
+
* @param {ConftestTarget} target опис таргета
|
|
291
|
+
* @param {string[]} files список файлів для перевірки (відносні до cwd)
|
|
292
|
+
* @returns {number} exit-код
|
|
293
|
+
*/
|
|
294
|
+
function runConftestForTarget(conftestBin, target, files) {
|
|
295
|
+
const policyAbs = join(POLICY_DIR, target.policyDir)
|
|
296
|
+
if (!existsSync(policyAbs)) {
|
|
297
|
+
return 0
|
|
298
|
+
}
|
|
299
|
+
console.log(`\n▶ conftest (${target.namespace} — ${files.length} файл(ів))`)
|
|
300
|
+
const r = spawnSync(
|
|
301
|
+
conftestBin,
|
|
302
|
+
['test', ...files, '-p', policyAbs, '--namespace', target.namespace, '--no-color'],
|
|
303
|
+
{ stdio: 'inherit', env: process.env }
|
|
304
|
+
)
|
|
305
|
+
if (r.error) {
|
|
306
|
+
console.error(`❌ Не вдалося запустити conftest: ${r.error.message}`)
|
|
307
|
+
return 1
|
|
308
|
+
}
|
|
309
|
+
return r.status ?? 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Запускає `conftest test` по всіх таргетах із `TARGETS`. Перший ненульовий exit-код
|
|
314
|
+
* запамʼятовується, але цикл йде до кінця, щоб користувач побачив усі порушення.
|
|
315
|
+
*
|
|
316
|
+
* Якщо `conftest` не знайдено в PATH — друкує `ℹ` повідомлення і повертає 0
|
|
317
|
+
* (структурні перевірки в `check-*.mjs` лишаються паралельно).
|
|
318
|
+
* @returns {number} 0 — все OK або skip; інакше — перший ненульовий exit-код
|
|
319
|
+
*/
|
|
320
|
+
export function runLintConftestCli() {
|
|
321
|
+
const conftestBin = resolveCmd('conftest')
|
|
322
|
+
if (!conftestBin) {
|
|
323
|
+
console.log(
|
|
324
|
+
'ℹ conftest не знайдено в PATH — пропускаю Rego-перевірки.\n' +
|
|
325
|
+
' Встанови, щоб запустити локально: brew install conftest (macOS) або https://www.conftest.dev/install/'
|
|
326
|
+
)
|
|
327
|
+
return 0
|
|
328
|
+
}
|
|
329
|
+
if (!existsSync(POLICY_DIR)) {
|
|
330
|
+
console.log(`ℹ Каталог Rego-полісі не знайдено (${POLICY_DIR}) — пропускаю conftest.`)
|
|
331
|
+
return 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const cwd = process.cwd()
|
|
335
|
+
const activeRules = loadActiveCursorRules(cwd)
|
|
336
|
+
let firstFailureCode = 0
|
|
337
|
+
for (const target of TARGETS) {
|
|
338
|
+
if (target.rule && activeRules && !activeRules.has(target.rule)) continue
|
|
339
|
+
const files = resolveTargetFiles(target, cwd)
|
|
340
|
+
if (files.length === 0) continue
|
|
341
|
+
const code = runConftestForTarget(conftestBin, target, files)
|
|
342
|
+
if (code !== 0 && firstFailureCode === 0) {
|
|
343
|
+
firstFailureCode = code
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return firstFailureCode
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
350
|
+
process.exit(runLintConftestCli())
|
|
351
|
+
}
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* повідомлення й продовжуємо з кодом 0. Структурні перевірки тих самих workflow паралельно живуть у
|
|
8
8
|
* `npm/scripts/check-ga.mjs`, тож відсутність conftest не пропускає порушення мовчки.
|
|
9
9
|
*
|
|
10
|
+
* Conftest проганяється у двох режимах:
|
|
11
|
+
* 1) per-workflow polysi (`ga.<name>`) — для канонічних `clean-ga-workflows`, `clean-merged-branch`,
|
|
12
|
+
* `lint-ga`, `git-ai`, що мають фіксовані поля (cron, ім'я кроку тощо);
|
|
13
|
+
* 2) `ga.workflow_common` — універсальні правила (concurrency, заборонені setup-bun/cache/install у
|
|
14
|
+
* кроках, shell line-continuation у `run:`, checkout перед локальним setup-bun-deps), які
|
|
15
|
+
* застосовуються до **кожного** `.github/workflows/*.yml`.
|
|
16
|
+
*
|
|
10
17
|
* Без preflight `actionlint` (через `bunx github-actionlint`) мовчки пропускає shell-перевірки в
|
|
11
18
|
* `run:` блоках, коли `shellcheck` відсутній у PATH; локально `bun lint-ga` лишається зеленим, а CI
|
|
12
19
|
* на ubuntu-latest (де shellcheck передвстановлений) падає. Preflight робить цю різницю явною.
|
|
@@ -16,7 +23,7 @@
|
|
|
16
23
|
*
|
|
17
24
|
* Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
|
|
18
25
|
*/
|
|
19
|
-
import { existsSync } from 'node:fs'
|
|
26
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
20
27
|
import { spawnSync } from 'node:child_process'
|
|
21
28
|
import { dirname, join } from 'node:path'
|
|
22
29
|
import { platform } from 'node:process'
|
|
@@ -46,6 +53,16 @@ const CONFTEST_TARGETS = [
|
|
|
46
53
|
workflow: '.github/workflows/clean-merged-branch.yml',
|
|
47
54
|
namespace: 'ga.clean_merged_branch',
|
|
48
55
|
label: 'clean-merged-branch.yml structure'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
workflow: '.github/workflows/lint-ga.yml',
|
|
59
|
+
namespace: 'ga.lint_ga',
|
|
60
|
+
label: 'lint-ga.yml structure'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
workflow: '.github/workflows/git-ai.yml',
|
|
64
|
+
namespace: 'ga.git_ai',
|
|
65
|
+
label: 'git-ai.yml structure'
|
|
49
66
|
}
|
|
50
67
|
]
|
|
51
68
|
|
|
@@ -234,5 +251,35 @@ function runConftestStep() {
|
|
|
234
251
|
])
|
|
235
252
|
if (code !== 0) return code
|
|
236
253
|
}
|
|
237
|
-
|
|
254
|
+
|
|
255
|
+
return runConftestWorkflowCommon(conftestBin)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Прогоняє `ga.workflow_common` на кожному `.github/workflows/*.yml` — універсальні перевірки
|
|
260
|
+
* (concurrency, заборонені setup-bun/cache/install, shell line-continuation, checkout перед
|
|
261
|
+
* локальним setup-bun-deps). Якщо директорії немає або файлів немає — мовчки skip.
|
|
262
|
+
*
|
|
263
|
+
* Викликаємо conftest на всіх файлах одним прогоном (`conftest test <files...>`) — швидше, ніж
|
|
264
|
+
* по одному, і summary-лог зрозуміліший. Перший ненульовий exit-код повертаємо як результат.
|
|
265
|
+
* @param {string} conftestBin абсолютний шлях до бінарника conftest
|
|
266
|
+
* @returns {number} 0 — OK, інакше exit-код conftest
|
|
267
|
+
*/
|
|
268
|
+
function runConftestWorkflowCommon(conftestBin) {
|
|
269
|
+
const wfDir = '.github/workflows'
|
|
270
|
+
if (!existsSync(wfDir)) return 0
|
|
271
|
+
const ymlFiles = readdirSync(wfDir)
|
|
272
|
+
.filter(f => f.endsWith('.yml'))
|
|
273
|
+
.map(f => join(wfDir, f))
|
|
274
|
+
if (ymlFiles.length === 0) return 0
|
|
275
|
+
|
|
276
|
+
return runStep('conftest (workflow_common — усі workflow)', conftestBin, [
|
|
277
|
+
'test',
|
|
278
|
+
...ymlFiles,
|
|
279
|
+
'-p',
|
|
280
|
+
GA_POLICY_DIR,
|
|
281
|
+
'--namespace',
|
|
282
|
+
'ga.workflow_common',
|
|
283
|
+
'--no-color'
|
|
284
|
+
])
|
|
238
285
|
}
|
package/scripts/lint-rego.mjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): preflight на `opa` і `regal`,
|
|
3
|
+
* далі послідовно `opa check --strict` і `regal lint`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Чому два інструменти:
|
|
6
|
+
* - `opa check --strict` — компіляція з типами і строгим режимом (мертвий код, неоднозначні
|
|
7
|
+
* правила, незадекларовані змінні). Ловить помилки, які `regal` навмисно лишає поза скоупом
|
|
8
|
+
* (він — про стиль і ідіоматичність, а не про компіляцію).
|
|
9
|
+
* - `regal lint` (https://docs.styra.com/regal) — статичний лінтер Rego: ловить v0-синтаксис,
|
|
10
|
+
* неявні set-rules та інші відхилення від `rego.v1`, плюс bugs/idiomatic/performance-правила.
|
|
11
|
+
*
|
|
12
|
+
* Без preflight-у на бінарники лінт мовчки злетить з невиразним повідомленням від shell —
|
|
13
|
+
* друкуємо явні install-hints (як це робить `lint-ga.mjs` для shellcheck/uv). `opa` додатково
|
|
14
|
+
* потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через `opa fmt`) — деталі в
|
|
15
|
+
* `mdc/rego.mdc`.
|
|
8
16
|
*
|
|
9
17
|
* Цілі лінту: `npm/policy/` (місце, де поки що живуть Rego-полісі пакета `@nitra/cursor`).
|
|
10
18
|
* Якщо в репозиторії з’являться інші *.rego поза цим деревом, додай шлях у `LINT_TARGETS` —
|
|
11
|
-
*
|
|
19
|
+
* обидва інструменти приймають кілька шляхів і самі рекурсивно обходять директорії.
|
|
12
20
|
*/
|
|
13
21
|
import { spawnSync } from 'node:child_process'
|
|
14
22
|
import { existsSync } from 'node:fs'
|
|
@@ -19,6 +27,24 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
|
19
27
|
/** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
|
|
20
28
|
const LINT_TARGETS = ['npm/policy']
|
|
21
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Друкує підказку зі встановлення `opa` (потрібен для `opa check --strict` і VS Code LSP).
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function printOpaInstallHints() {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
[
|
|
37
|
+
'❌ opa не знайдено в PATH.',
|
|
38
|
+
' Без нього не запускається `opa check --strict` (типи + мертвий код у *.rego),',
|
|
39
|
+
' і не працює VS Code-розширення `tsandall.opa` (LSP, format-on-save через opa fmt).',
|
|
40
|
+
' Встанови:',
|
|
41
|
+
' macOS: brew install opa',
|
|
42
|
+
' Universal: https://www.openpolicyagent.org/docs/latest/#1-download-opa',
|
|
43
|
+
''
|
|
44
|
+
].join('\n')
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
/**
|
|
23
49
|
* Друкує підказку зі встановлення `regal`.
|
|
24
50
|
* @returns {void}
|
|
@@ -29,7 +55,7 @@ function printRegalInstallHints() {
|
|
|
29
55
|
'❌ regal не знайдено в PATH.',
|
|
30
56
|
' Без нього не перевіряється rego.v1 синтаксис у *.rego (правило `conftest`).',
|
|
31
57
|
' Встанови:',
|
|
32
|
-
' macOS:
|
|
58
|
+
' macOS: brew install regal',
|
|
33
59
|
' Universal: https://docs.styra.com/regal#installation',
|
|
34
60
|
''
|
|
35
61
|
].join('\n')
|
|
@@ -37,34 +63,54 @@ function printRegalInstallHints() {
|
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
/**
|
|
40
|
-
* Запускає
|
|
66
|
+
* Запускає крок з відображенням команди користувачу. Stdout/stderr передаємо як є
|
|
67
|
+
* (`stdio: 'inherit'`), щоб виглядало як прямий виклик у shell.
|
|
68
|
+
* @param {string} bin абсолютний шлях до бінарника
|
|
69
|
+
* @param {string[]} args аргументи
|
|
70
|
+
* @param {string} cwd робочий каталог
|
|
71
|
+
* @returns {number} код виходу (0 — OK)
|
|
72
|
+
*/
|
|
73
|
+
function runStep(bin, args, cwd) {
|
|
74
|
+
console.log(`▶ ${bin} ${args.join(' ')}`)
|
|
75
|
+
const result = spawnSync(bin, args, { cwd, stdio: 'inherit', env: process.env })
|
|
76
|
+
if (result.error) {
|
|
77
|
+
process.stderr.write(`❌ Не вдалося запустити ${bin}: ${result.error.message}\n`)
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
return result.status ?? 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Запускає `opa check --strict` і `regal lint` по існуючих цілях. Якщо жодної цілі немає —
|
|
85
|
+
* пропускає лінт із кодом 0. Якщо хоча б один preflight не пройшов — exit 1 ще до запусків.
|
|
41
86
|
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
42
|
-
* @returns {number} 0 — OK або skip; інакше код виходу
|
|
87
|
+
* @returns {number} 0 — OK або skip; інакше код виходу першого кроку, що впав
|
|
43
88
|
*/
|
|
44
89
|
export function runLintRego(cwd = process.cwd()) {
|
|
45
90
|
const root = resolve(cwd)
|
|
91
|
+
const opa = resolveCmd('opa')
|
|
46
92
|
const regal = resolveCmd('regal')
|
|
93
|
+
|
|
94
|
+
let preflightOk = true
|
|
95
|
+
if (!opa) {
|
|
96
|
+
printOpaInstallHints()
|
|
97
|
+
preflightOk = false
|
|
98
|
+
}
|
|
47
99
|
if (!regal) {
|
|
48
100
|
printRegalInstallHints()
|
|
49
|
-
|
|
101
|
+
preflightOk = false
|
|
50
102
|
}
|
|
103
|
+
if (!preflightOk) return 1
|
|
51
104
|
|
|
52
105
|
const targets = LINT_TARGETS.filter(rel => existsSync(resolve(root, rel)))
|
|
53
106
|
if (targets.length === 0) {
|
|
54
107
|
return 0
|
|
55
108
|
}
|
|
56
109
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
env: process.env
|
|
62
|
-
})
|
|
63
|
-
if (result.error) {
|
|
64
|
-
process.stderr.write(`❌ Не вдалося запустити regal: ${result.error.message}\n`)
|
|
65
|
-
return 1
|
|
66
|
-
}
|
|
67
|
-
return result.status ?? 1
|
|
110
|
+
const opaCode = runStep(opa, ['check', '--strict', ...targets], root)
|
|
111
|
+
if (opaCode !== 0) return opaCode
|
|
112
|
+
|
|
113
|
+
return runStep(regal, ['lint', ...targets], root)
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
process.exitCode = runLintRego()
|
|
@@ -80,17 +80,14 @@ export function listShellScriptPaths(cwd) {
|
|
|
80
80
|
return []
|
|
81
81
|
}
|
|
82
82
|
const files = ls.stdout.split('\0').filter(Boolean)
|
|
83
|
-
return
|
|
83
|
+
return new Set(files).toSorted()
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const fromGlob = globSync('**/*.sh', {
|
|
87
87
|
cwd,
|
|
88
|
-
exclude: p =>
|
|
89
|
-
p.includes('node_modules') ||
|
|
90
|
-
p.startsWith(`node_modules/`) ||
|
|
91
|
-
p.split('/').includes('node_modules')
|
|
88
|
+
exclude: p => p.includes('node_modules') || p.startsWith(`node_modules/`) || p.split('/').includes('node_modules')
|
|
92
89
|
})
|
|
93
|
-
return
|
|
90
|
+
return new Set(fromGlob.map(p => p.replaceAll('\\', '/'))).toSorted()
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
/**
|
|
@@ -19,11 +19,7 @@
|
|
|
19
19
|
import { readdir, readFile } from 'node:fs/promises'
|
|
20
20
|
import { join, relative } from 'node:path'
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
flattenWorkflowSteps,
|
|
24
|
-
getStepRun,
|
|
25
|
-
parseWorkflowYaml
|
|
26
|
-
} from './gha-workflow.mjs'
|
|
22
|
+
import { flattenWorkflowSteps, getStepRun, parseWorkflowYaml } from './gha-workflow.mjs'
|
|
27
23
|
|
|
28
24
|
const WORKFLOWS_DIR_REL = '.github/workflows'
|
|
29
25
|
const REQUIRED_IGNORES = ['graphql', 'bun']
|
|
@@ -122,7 +118,7 @@ export function evaluateDepcheckStepForPackage(root, pkgRoot) {
|
|
|
122
118
|
// Усі знайдені кроки існують, але жоден не має повного списку обов'язкових ignores —
|
|
123
119
|
// повертаємо missing з першого, щоб дати конкретний фідбек.
|
|
124
120
|
const firstMissing = REQUIRED_IGNORES.filter(
|
|
125
|
-
req => !(
|
|
121
|
+
req => !(parseDepcheckIgnoresArg(stepsForThisPackage[0].args) ?? []).includes(req)
|
|
126
122
|
)
|
|
127
123
|
return { kind: 'missing-ignores', missing: firstMissing }
|
|
128
124
|
}
|