@nitra/cursor 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/hooks/capture-decisions.sh +1 -1
- package/.claude-template/hooks/normalize-decisions.sh +8 -4
- package/CHANGELOG.md +33 -0
- package/bin/n-cursor.js +53 -0
- package/package.json +1 -1
- package/rules/adr/adr.mdc +5 -5
- package/rules/adr/js/templates/hooks/.gitignore.snippet +1 -0
- package/rules/changelog/changelog.mdc +1 -1
- package/rules/changelog/js/consistency.mjs +69 -12
- package/rules/ci4/ci4.mdc +2 -2
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/js/lint.mjs +1 -1
- package/rules/docker/lib/docker-hadolint.mjs +27 -55
- package/rules/ga/lint/lint.mjs +18 -54
- package/rules/js-run/js/runtime.mjs +32 -0
- package/rules/js-run/js-run.mdc +6 -0
- package/rules/js-run/lib/temporal-scan.mjs +52 -0
- package/rules/k8s/lint/lint.mjs +3 -10
- package/rules/nginx-default-tpl/js/template.mjs +39 -1
- package/rules/nginx-default-tpl/nginx-default-tpl.mdc +3 -1
- package/rules/npm-module/js/skill_meta.mjs +12 -0
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/target.json +1 -0
- package/rules/rego/lint/lint.mjs +10 -55
- package/rules/release/change.mjs +34 -5
- package/rules/release/lib/change-file.mjs +26 -11
- package/rules/text/lint/lint.mjs +11 -40
- package/rules/worktree/policy/vscode_settings/target.json +5 -0
- package/rules/worktree/policy/vscode_settings/template/settings.json.snippet.json +8 -0
- package/rules/worktree/policy/zed_settings/target.json +5 -0
- package/rules/worktree/policy/zed_settings/template/settings.json.snippet.json +12 -0
- package/rules/worktree/worktree.mdc +52 -0
- package/schemas/target.json +5 -0
- package/scripts/lib/assert-project-root.mjs +80 -0
- package/scripts/lib/ensure-tool.mjs +352 -0
- package/scripts/lib/root-notice.mjs +64 -0
- package/scripts/lib/run-conftest-batch.mjs +6 -28
- package/scripts/lib/run-rule.mjs +61 -5
- package/scripts/lib/skill-meta.mjs +16 -2
- package/scripts/lib/template.mjs +29 -3
- package/scripts/lib/worktree-notice.mjs +121 -73
- package/scripts/sync-claude-config.mjs +2 -2
- package/skills/fix/SKILL.md +4 -4
- package/skills/llm-patch/meta.json +1 -1
- package/skills/publish-telegram/meta.json +1 -1
- package/skills/start-check/meta.json +1 -1
- package/skills/worktree/meta.json +1 -1
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/npm-module/policy/npm_publish_yml/npm_publish_yml.rego +0 -87
package/rules/ga/lint/lint.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-обгортка над канонічним `lint-ga` (ga.mdc):
|
|
3
|
-
*
|
|
2
|
+
* CLI-обгортка над канонічним `lint-ga` (ga.mdc): авто-встановлює `shellcheck` і `conftest`
|
|
3
|
+
* через `ensureTool` (brew/scoop/GitHub Release per-platform), перевіряє наявність `uv` (для `uvx`),
|
|
4
4
|
* тоді послідовно виконує `bunx github-actionlint`, `uvx zizmor --offline --collect=workflows .` і
|
|
5
5
|
* делегує до `rules/ga/fix.mjs::check()` — там і Rego-частина (через `runConftestBatch`),
|
|
6
6
|
* і JS cross-file перевірки правил `ga.mdc`.
|
|
@@ -13,14 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Без preflight `actionlint` (через `bunx github-actionlint`) мовчки пропускає shell-перевірки в
|
|
15
15
|
* `run:` блоках, коли `shellcheck` відсутній у PATH; локально `bun lint-ga` лишається зеленим, а CI
|
|
16
|
-
* на ubuntu-latest (де shellcheck передвстановлений) падає.
|
|
16
|
+
* на ubuntu-latest (де shellcheck передвстановлений) падає. ensureTool('shellcheck') усуває цю різницю.
|
|
17
17
|
*
|
|
18
|
-
* `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `conftest` потрібен для `rules/ga/fix.mjs::runAllGaRego` (`runConftestBatch`). Без preflight крок
|
|
22
|
-
* check-ga кидає виняток, який глобальний `catch` у `bin/n-cursor.js` раніше ковтав без логу —
|
|
23
|
-
* локально це виглядало як мовчазний exit 1.
|
|
18
|
+
* `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає неінформативно; підказка
|
|
19
|
+
* з командою встановлення коротша й корисніша. `uv` не в реєстрі ensureTool → hint-only.
|
|
24
20
|
*
|
|
25
21
|
* Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
|
|
26
22
|
*
|
|
@@ -33,6 +29,7 @@ import { check as checkGa } from '../js/workflows.mjs'
|
|
|
33
29
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
34
30
|
import { runLintStep } from '../../../scripts/lib/run-lint-step.mjs'
|
|
35
31
|
import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
|
|
32
|
+
import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
35
|
* Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
|
|
@@ -44,23 +41,6 @@ import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
|
|
|
44
41
|
* @property {string} successMsg повідомлення на pass-шлях
|
|
45
42
|
*/
|
|
46
43
|
|
|
47
|
-
/** @type {PreflightDep} */
|
|
48
|
-
const SHELLCHECK_PREFLIGHT = {
|
|
49
|
-
bin: 'shellcheck',
|
|
50
|
-
winBins: ['shellcheck.exe'],
|
|
51
|
-
explanation: [
|
|
52
|
-
'Без нього `actionlint` пропускає shell-перевірки в run: блоках,',
|
|
53
|
-
'тож локальний прогін зеленіє, а CI на ubuntu-latest (де shellcheck',
|
|
54
|
-
'передвстановлений) падає на тих самих workflow.'
|
|
55
|
-
].join('\n '),
|
|
56
|
-
install: [
|
|
57
|
-
'macOS: brew install shellcheck',
|
|
58
|
-
'Debian/Ubuntu: sudo apt-get install -y shellcheck',
|
|
59
|
-
'Arch: sudo pacman -S shellcheck'
|
|
60
|
-
],
|
|
61
|
-
successMsg: '✅ shellcheck знайдено в PATH — actionlint виконуватиме SC-правила, як у CI'
|
|
62
|
-
}
|
|
63
|
-
|
|
64
44
|
/** @type {PreflightDep} */
|
|
65
45
|
const UV_PREFLIGHT = {
|
|
66
46
|
bin: 'uv',
|
|
@@ -77,18 +57,6 @@ const UV_PREFLIGHT = {
|
|
|
77
57
|
successMsg: '✅ uv знайдено в PATH — uvx zizmor запуститься'
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
/** @type {PreflightDep} */
|
|
81
|
-
const CONFTEST_PREFLIGHT = {
|
|
82
|
-
bin: 'conftest',
|
|
83
|
-
winBins: ['conftest.exe'],
|
|
84
|
-
explanation: [
|
|
85
|
-
'Без нього не запускається пер-документна валідація через rego-полісі (npm/rules/*/policy/)',
|
|
86
|
-
'у кроці check-ga — `runConftestBatch` завершується hard-fail.'
|
|
87
|
-
].join('\n '),
|
|
88
|
-
install: ['macOS: brew install conftest', 'Universal: https://www.conftest.dev/install/'],
|
|
89
|
-
successMsg: '✅ conftest знайдено в PATH — check-ga виконає rego-полісі через runConftestBatch'
|
|
90
|
-
}
|
|
91
|
-
|
|
92
60
|
/**
|
|
93
61
|
* Шукає бінарник у PATH з урахуванням Windows: спершу `winBins`, потім `bin`.
|
|
94
62
|
* @param {PreflightDep} dep опис залежності
|
|
@@ -134,29 +102,25 @@ function preflight(dep) {
|
|
|
134
102
|
}
|
|
135
103
|
|
|
136
104
|
/**
|
|
137
|
-
* Виконує канонічний `lint-ga`
|
|
105
|
+
* Виконує канонічний `lint-ga` — авто-встановлює shellcheck/conftest, перевіряє uv, запускає actionlint/zizmor/check-ga.
|
|
138
106
|
*
|
|
139
107
|
* Послідовність:
|
|
140
|
-
* 1)
|
|
141
|
-
* 2) `
|
|
142
|
-
* 3) `
|
|
143
|
-
* 4) `
|
|
108
|
+
* 1) ensureTool: `shellcheck` і `conftest` (авто-install або hard-fail);
|
|
109
|
+
* 2) preflight: `uv` (для `uvx zizmor`) — hint-only, без авто-install;
|
|
110
|
+
* 3) `bunx github-actionlint`;
|
|
111
|
+
* 4) `uvx zizmor --offline --collect=workflows .`;
|
|
112
|
+
* 5) `rules/ga/fix.mjs::check()` — Rego-полісі (батч conftest з `npm/policy/ga/`) + JS cross-file
|
|
144
113
|
* перевірки правил `ga.mdc`. Це **те саме**, що робить `npx \@nitra/cursor check ga`, тож
|
|
145
114
|
* `lint-ga` тепер є суперсетом перевірки правила: external-tools + check.
|
|
146
|
-
*
|
|
147
|
-
* Якщо хоча б один preflight не пройшов — виходимо одразу з кодом 1, **до** запуску actionlint/zizmor,
|
|
148
|
-
* бо їхні власні повідомлення про відсутність залежностей менш інформативні (особливо для shellcheck —
|
|
149
|
-
* actionlint мовчки пропускає SC-правила; ця перевірка — головний сенс обгортки).
|
|
150
|
-
*
|
|
151
|
-
* Першу помилку від actionlint/zizmor/check повертаємо як код виходу; наступні кроки не запускаються.
|
|
152
115
|
* @returns {Promise<number>} 0 — все OK, інакше — код першого кроку, що впав
|
|
153
116
|
*/
|
|
154
117
|
async function runLintGaSteps() {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
118
|
+
// Auto-install: throws on failure → propagates as exit 1 from runStandardLint
|
|
119
|
+
ensureTool('shellcheck')
|
|
120
|
+
ensureTool('conftest')
|
|
121
|
+
|
|
122
|
+
// uv is hint-only (not in auto-install registry)
|
|
123
|
+
if (!preflight(UV_PREFLIGHT)) return 1
|
|
160
124
|
|
|
161
125
|
const actionlintCode = runLintStep('actionlint', 'bunx', ['github-actionlint'])
|
|
162
126
|
if (actionlintCode !== 0) return actionlintCode
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* - «Паузи через setTimeout»: `new Promise(resolve => setTimeout(resolve, ms))` (з/без `await`)
|
|
27
27
|
* треба замінити на `await setTimeout(ms)` з `node:timers/promises`
|
|
28
28
|
* (див. `utils/promise-settimeout-scan.mjs`);
|
|
29
|
+
* - «Temporal у Bun runtime»: identifier `Temporal` заборонений, бо поточний Bun runtime
|
|
30
|
+
* не має глобального Temporal API (див. `utils/temporal-scan.mjs`);
|
|
29
31
|
* - «jsconfig.json»: у backend-пакеті з каталогом `src/` у корені має бути `jsconfig.json`,
|
|
30
32
|
* вміст якого збігається з каноном js-run.mdc (NodeNext і include на дерево `src`).
|
|
31
33
|
*
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
} from '../lib/conn-imports-scan.mjs'
|
|
51
53
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
52
54
|
import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '../lib/promise-settimeout-scan.mjs'
|
|
55
|
+
import { findTemporalUsageInText, isTemporalScanSourceFile } from '../lib/temporal-scan.mjs'
|
|
53
56
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
54
57
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
55
58
|
|
|
@@ -313,6 +316,30 @@ async function checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, f
|
|
|
313
316
|
return violations
|
|
314
317
|
}
|
|
315
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Сканує джерела пакета на `Temporal`, який у Bun runtime ще недоступний.
|
|
321
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
322
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів
|
|
323
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
324
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
325
|
+
* @returns {Promise<number>} кількість порушень
|
|
326
|
+
*/
|
|
327
|
+
async function checkTemporalUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
328
|
+
let violations = 0
|
|
329
|
+
for (const absPath of sourcePaths) {
|
|
330
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
331
|
+
if (!isTemporalScanSourceFile(rel)) continue
|
|
332
|
+
const content = await readFile(absPath, 'utf8')
|
|
333
|
+
for (const v of findTemporalUsageInText(content, rel)) {
|
|
334
|
+
violations++
|
|
335
|
+
fail(
|
|
336
|
+
`${label}${rel}:${v.line} — Temporal API заборонений у Bun runtime; використовуй Date або інʼєктований timestamp`
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return violations
|
|
341
|
+
}
|
|
342
|
+
|
|
316
343
|
/**
|
|
317
344
|
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
318
345
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
@@ -371,6 +398,11 @@ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn, cwd) {
|
|
|
371
398
|
passFn(`${label}немає 'new Promise(r => setTimeout(r, ms))' — паузи через 'node:timers/promises'`)
|
|
372
399
|
}
|
|
373
400
|
|
|
401
|
+
const temporalViolations = await checkTemporalUsage(absPackageRoot, sourcePaths, label, fail)
|
|
402
|
+
if (temporalViolations === 0) {
|
|
403
|
+
passFn(`${label}немає Temporal API у Bun runtime-коді`)
|
|
404
|
+
}
|
|
405
|
+
|
|
374
406
|
checkOtelConfigmap(rootDir, passFn, cwd)
|
|
375
407
|
}
|
|
376
408
|
|
package/rules/js-run/js-run.mdc
CHANGED
|
@@ -30,6 +30,12 @@ version: '1.11'
|
|
|
30
30
|
|
|
31
31
|
Це **не** стосується поля `engines.node` (мінімальна версія Node для сумісності інструментів) і **не** стосується frontend-пакетів з `vite` у `devDependencies`.
|
|
32
32
|
|
|
33
|
+
## Temporal API
|
|
34
|
+
|
|
35
|
+
У backend/Bun runtime-коді **не використовуй `Temporal`** (`Temporal.Now`, `Temporal.Instant`, імпорти з polyfill тощо). Поточний Bun runtime ще не має глобального `Temporal` (`typeof Temporal === "undefined"`), тому агентам треба лишатися на сумісному `Date` API або передавати timestamp у чисті функції через параметр.
|
|
36
|
+
|
|
37
|
+
Перевірка `npx @nitra/cursor fix js-run` сканує JS/TS AST і падає на identifier `Temporal` у backend workspace-коді.
|
|
38
|
+
|
|
33
39
|
Канон заборонених патернів у `scripts`: [package.json.deny.json](./policy/package_json/template/package.json.deny.json) (`scriptsForbidden`).
|
|
34
40
|
|
|
35
41
|
## Структура проекту
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-сканер заборони `Temporal` у Bun runtime-коді.
|
|
3
|
+
*
|
|
4
|
+
* Bun 1.3.x ще не має глобального `Temporal`, тому правило js-run забороняє
|
|
5
|
+
* будь-який identifier `Temporal` у backend workspace-коді. Заборона свідомо
|
|
6
|
+
* охоплює polyfill/import-сценарії: у цьому репозиторії канон для часу лишається
|
|
7
|
+
* через `Date` або ін'єкцію timestamp у чисті функції.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
normalizeSnippet,
|
|
11
|
+
offsetToLine,
|
|
12
|
+
parseProgramOrNull,
|
|
13
|
+
walkAstWithAncestors
|
|
14
|
+
} from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
15
|
+
|
|
16
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Знаходить використання identifier `Temporal` у тексті.
|
|
20
|
+
* @param {string} content вихідний код
|
|
21
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
22
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
23
|
+
*/
|
|
24
|
+
export function findTemporalUsageInText(content, virtualPath = 'scan.ts') {
|
|
25
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
26
|
+
if (!program) return []
|
|
27
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
28
|
+
const out = []
|
|
29
|
+
/** @type {Set<string>} */
|
|
30
|
+
const seen = new Set()
|
|
31
|
+
walkAstWithAncestors(program, [], node => {
|
|
32
|
+
if (node.type !== 'Identifier' || node.name !== 'Temporal') return
|
|
33
|
+
const key = `${node.start}:${node.end}`
|
|
34
|
+
if (seen.has(key)) return
|
|
35
|
+
seen.add(key)
|
|
36
|
+
out.push({
|
|
37
|
+
line: offsetToLine(content, node.start),
|
|
38
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, виключно з `.d.ts`).
|
|
46
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
47
|
+
* @returns {boolean} `true`, якщо розширення підходить для сканування
|
|
48
|
+
*/
|
|
49
|
+
export function isTemporalScanSourceFile(relativePath) {
|
|
50
|
+
if (!SOURCE_FILE_RE.test(relativePath)) return false
|
|
51
|
+
return !relativePath.endsWith('.d.ts')
|
|
52
|
+
}
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { basename, dirname, join, relative } from 'node:path'
|
|
|
24
24
|
import { parse } from 'yaml'
|
|
25
25
|
|
|
26
26
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
27
|
+
import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
|
|
27
28
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
28
29
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
29
30
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
@@ -123,11 +124,7 @@ function runKubeconform(dirs) {
|
|
|
123
124
|
'-ignore-missing-schemas',
|
|
124
125
|
...dirs
|
|
125
126
|
]
|
|
126
|
-
const kubeconformPath =
|
|
127
|
-
if (!kubeconformPath) {
|
|
128
|
-
console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
|
|
129
|
-
return 127
|
|
130
|
-
}
|
|
127
|
+
const kubeconformPath = ensureTool('kubeconform')
|
|
131
128
|
const r = spawnSync(kubeconformPath, args, { stdio: 'inherit', shell: false })
|
|
132
129
|
if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
|
|
133
130
|
console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
|
|
@@ -297,11 +294,7 @@ async function runKubescape(dirs, root) {
|
|
|
297
294
|
if (exceptionsArgs.length > 0) {
|
|
298
295
|
console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
|
|
299
296
|
}
|
|
300
|
-
const kubescapePath =
|
|
301
|
-
if (!kubescapePath) {
|
|
302
|
-
console.error(KUBESCAPE_MISSING_HINT)
|
|
303
|
-
return 127
|
|
304
|
-
}
|
|
297
|
+
const kubescapePath = ensureTool('kubescape')
|
|
305
298
|
let kubectlPath = null
|
|
306
299
|
for (const d of dirs) {
|
|
307
300
|
const kdirs = await findKustomizationDirs(d)
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* У дереві від **cwd** усі **default.tpl.conf** стають **default.conf.template**: перейменування, або
|
|
13
13
|
* якщо **default.conf.template** уже є — він перезаписується вмістом **default.tpl.conf**, після чого
|
|
14
14
|
* **default.tpl.conf** видаляється. Якщо після міграції шаблону немає — перевірка пропускається (0).
|
|
15
|
+
*
|
|
16
|
+
* Невалідна директива **`error_log off;`** (nginx трактує "off" як ім'я файлу `/etc/nginx/off` і падає під
|
|
17
|
+
* readOnlyRootFilesystem) автоматично замінюється на **`error_log /dev/null crit;`** у кожному шаблоні.
|
|
15
18
|
*/
|
|
16
19
|
import { existsSync } from 'node:fs'
|
|
17
20
|
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -33,6 +36,11 @@ const FIND_CMD_RE = /\bfind\b/u
|
|
|
33
36
|
const GZIP_CMD_RE = /\bgzip\b/u
|
|
34
37
|
const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
|
|
35
38
|
|
|
39
|
+
// `error_log off;` — НЕ валідний nginx: "off" трактується як ім'я файлу (/etc/nginx/off)
|
|
40
|
+
// і падає під readOnlyRootFilesystem. /dev/null — writable device, тому канон — `error_log /dev/null crit;`.
|
|
41
|
+
const ERROR_LOG_OFF_RE = /error_log\s+off\s*;/gu
|
|
42
|
+
const ERROR_LOG_CANONICAL = 'error_log /dev/null crit;'
|
|
43
|
+
|
|
36
44
|
/**
|
|
37
45
|
* Збирає абсолютні шляхи до **default.conf.template** у репозиторії; будь-який сегмент
|
|
38
46
|
* `fixtures/` у шляху виключається — це тестові артефакти (як `tests/fixtures/` так і
|
|
@@ -98,6 +106,28 @@ export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
|
|
|
98
106
|
return { renamed, overwritten }
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Замінює невалідну директиву `error_log off;` на `error_log /dev/null crit;` у всіх
|
|
111
|
+
* **default.conf.template** від `root`. `error_log off;` — НЕ валідний nginx: "off" трактується
|
|
112
|
+
* як ім'я файлу (`/etc/nginx/off`) і падає під readOnlyRootFilesystem; `/dev/null` — writable device.
|
|
113
|
+
* @param {string} root корінь обходу (зазвичай cwd репозиторію)
|
|
114
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
115
|
+
* @returns {Promise<string[]>} відносні шляхи виправлених шаблонів (для звіту)
|
|
116
|
+
*/
|
|
117
|
+
export async function migrateErrorLogOffDirective(root, ignorePaths = []) {
|
|
118
|
+
const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
|
|
119
|
+
/** @type {string[]} */
|
|
120
|
+
const fixed = []
|
|
121
|
+
for (const abs of templates) {
|
|
122
|
+
const body = await readFile(abs, 'utf8')
|
|
123
|
+
const next = body.replace(ERROR_LOG_OFF_RE, ERROR_LOG_CANONICAL)
|
|
124
|
+
if (next === body) continue
|
|
125
|
+
await writeFile(abs, next, 'utf8')
|
|
126
|
+
fixed.push(relative(root, abs).replaceAll('\\', '/') || abs)
|
|
127
|
+
}
|
|
128
|
+
return fixed
|
|
129
|
+
}
|
|
130
|
+
|
|
101
131
|
/**
|
|
102
132
|
* Імена змінних з ini (рядки KEY=value, без коментарів і порожніх).
|
|
103
133
|
* @param {string} iniText вміст *.ini
|
|
@@ -131,7 +161,10 @@ export function nginxTemplateViolations(content) {
|
|
|
131
161
|
{ msg: 'відсутнє listen 8080', ok: c => c.includes('listen 8080') },
|
|
132
162
|
{ msg: 'відсутнє server_name _', ok: c => c.includes('server_name _') },
|
|
133
163
|
{ msg: 'відсутнє access_log off', ok: c => c.includes('access_log off') },
|
|
134
|
-
{
|
|
164
|
+
{
|
|
165
|
+
msg: 'відсутнє error_log /dev/null crit (error_log off — НЕ валідний nginx, падає під readOnlyRootFilesystem)',
|
|
166
|
+
ok: c => c.includes('error_log /dev/null crit')
|
|
167
|
+
},
|
|
135
168
|
{ msg: 'відсутнє root /usr/share/nginx/html', ok: c => c.includes('root /usr/share/nginx/html') },
|
|
136
169
|
{
|
|
137
170
|
msg: 'location /healthz має повертати healthy (див. nginx-default-tpl.mdc)',
|
|
@@ -416,6 +449,11 @@ export async function check(cwd = process.cwd()) {
|
|
|
416
449
|
pass(`Перезаписано default.conf.template змістом default.tpl.conf: ${rel}`)
|
|
417
450
|
}
|
|
418
451
|
|
|
452
|
+
const errorLogFixed = await migrateErrorLogOffDirective(root, ignorePaths)
|
|
453
|
+
for (const rel of errorLogFixed) {
|
|
454
|
+
pass(`Замінено невалідне error_log off; → error_log /dev/null crit; у ${rel}`)
|
|
455
|
+
}
|
|
456
|
+
|
|
419
457
|
const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
|
|
420
458
|
|
|
421
459
|
if (templates.length === 0) {
|
|
@@ -19,7 +19,9 @@ server {
|
|
|
19
19
|
|
|
20
20
|
# disable all log
|
|
21
21
|
access_log off;
|
|
22
|
-
error_log off
|
|
22
|
+
# `error_log off;` — НЕ валідний nginx: "off" трактується як ім'я файлу (/etc/nginx/off)
|
|
23
|
+
# і падає під readOnlyRootFilesystem. /dev/null — writable device.
|
|
24
|
+
error_log /dev/null crit;
|
|
23
25
|
|
|
24
26
|
# This would be the directory where your Vite app's static files are stored at
|
|
25
27
|
root /usr/share/nginx/html;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Кожен `npm/skills/<id>/` має містити валідний `meta.json`:
|
|
5
5
|
* - `worktree` присутнє і boolean;
|
|
6
6
|
* - `auto` (якщо присутнє) — розпізнане (`"завжди"` або непорожній масив рядків);
|
|
7
|
+
* - `requireRoot` (якщо присутнє) — boolean; не може бути `false` при `worktree:true`
|
|
8
|
+
* (worktree вже вимагає кореня — суперечність вводить в оману);
|
|
7
9
|
* - залишковий `auto.md` заборонено (міграція на meta.json завершена).
|
|
8
10
|
*
|
|
9
11
|
* Концерн застосовний лише в репо самого пакета (де є `npm/skills/`); у споживача
|
|
@@ -52,6 +54,16 @@ export function check(cwd = process.cwd()) {
|
|
|
52
54
|
reporter.fail(`skills/${id}: meta.json.auto нерозпізнане — очікується "завжди" або непорожній масив правил`)
|
|
53
55
|
skillOk = false
|
|
54
56
|
}
|
|
57
|
+
if (raw.requireRoot !== undefined && typeof raw.requireRoot !== 'boolean') {
|
|
58
|
+
reporter.fail(`skills/${id}: meta.json.requireRoot має бути boolean`)
|
|
59
|
+
skillOk = false
|
|
60
|
+
}
|
|
61
|
+
if (raw.worktree === true && raw.requireRoot === false) {
|
|
62
|
+
reporter.fail(
|
|
63
|
+
`skills/${id}: requireRoot:false суперечить worktree:true (worktree вже вимагає кореня — прибери поле)`
|
|
64
|
+
)
|
|
65
|
+
skillOk = false
|
|
66
|
+
}
|
|
55
67
|
if (skillOk) {
|
|
56
68
|
reporter.pass(`skills/${id}: meta.json валідний`)
|
|
57
69
|
}
|
|
@@ -65,7 +65,7 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
|
|
|
65
65
|
|
|
66
66
|
**`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).
|
|
67
67
|
|
|
68
|
-
Workflow робить **release + publish** одним job (`release-publish`): крок **`Release (bump + CHANGELOG + tag)`** (`node npm/bin/n-cursor.js release` — агрегує change-файли, bump `version`, генерує секцію `CHANGELOG.md`, ставить git-тег) виконується **перед** публікацією. Тому потрібні **`permissions.contents: write`** і **`persist-credentials: true`** з **`fetch-depth: 0`** на `checkout` (release пушить commit-back версії та тег), а також локальний composite **`./.github/actions/setup-bun-deps`** і крок `Configure git identity`. Це узгоджено з **`n-changelog`**: `version`/`CHANGELOG.md` змінює лише `n-cursor release` у CI на `main`. Програмна перевірка (`npm_module.npm_publish_yml`)
|
|
68
|
+
Workflow робить **release + publish** одним job (`release-publish`): крок **`Release (bump + CHANGELOG + tag)`** (`node npm/bin/n-cursor.js release` — агрегує change-файли, bump `version`, генерує секцію `CHANGELOG.md`, ставить git-тег) виконується **перед** публікацією. Тому потрібні **`permissions.contents: write`** і **`persist-credentials: true`** з **`fetch-depth: 0`** на `checkout` (release пушить commit-back версії та тег), а також локальний composite **`./.github/actions/setup-bun-deps`** і крок `Configure git identity`. Це узгоджено з **`n-changelog`**: `version`/`CHANGELOG.md` змінює лише `n-cursor release` у CI на `main`. Програмна перевірка (`npm_module.npm_publish_yml`) звіряє **весь канонічний сніпет** напряму (`target.json:"check":"template"`, generic deep-subset): усі поля й кроки сніпета (`on.push.paths`/`branches`, `concurrency`, `permissions.contents/id-token`, `checkout` з `persist-credentials/fetch-depth`, `setup-bun-deps`, `Configure git identity`, `Release`, publish-крок) **обовʼязкові**; зайві кроки/поля дозволені (subset-of), масиви матчаться за наявністю (порядок кроків не важить). Сніпет — єдине джерело істини: його редагування одразу змінює enforce, без правок rego й без міграторів.
|
|
69
69
|
|
|
70
70
|
- Канон: [npm-publish.yml.snippet.yml](./policy/npm_publish_yml/template/npm-publish.yml.snippet.yml)
|
|
71
71
|
|
package/rules/rego/lint/lint.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`):
|
|
3
|
-
* далі послідовно `opa check --strict`,
|
|
4
|
-
* `conftest verify` (для `*_test.rego`-файлів) якщо conftest у PATH.
|
|
2
|
+
* Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): `ensureTool` на `opa` і `regal`
|
|
3
|
+
* (авто-install per-platform або hard-fail), далі послідовно `opa check --strict`,
|
|
4
|
+
* `regal lint` і опційний `conftest verify` (для `*_test.rego`-файлів) якщо conftest у PATH.
|
|
5
5
|
*
|
|
6
6
|
* Чому два-три інструменти:
|
|
7
7
|
* - `opa check --strict` — компіляція з типами і строгим режимом (мертвий код, неоднозначні
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* Якщо conftest відсутній у PATH — пропускаємо без помилки (тести опційні в локальному середовищі;
|
|
14
14
|
* у CI потрібно встановити conftest).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через
|
|
19
|
-
* `mdc/rego.mdc`.
|
|
16
|
+
* `opa`/`regal` резолвляться через `ensureTool` (PATH → кеш → авто-install brew/scoop/GitHub
|
|
17
|
+
* Release → hard-fail) — без них лінт мовчки злетів би з невиразним повідомленням від shell.
|
|
18
|
+
* `opa` додатково потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через
|
|
19
|
+
* `opa fmt`) — деталі в `mdc/rego.mdc`.
|
|
20
20
|
*
|
|
21
21
|
* Цілі лінту: `npm/rules/` (де живуть Rego-полісі пакета `@nitra/cursor` — у
|
|
22
22
|
* `npm/rules/<id>/policy/<concern>/`). Усі три інструменти приймають один шлях
|
|
@@ -31,47 +31,13 @@ import { existsSync } from 'node:fs'
|
|
|
31
31
|
import { resolve } from 'node:path'
|
|
32
32
|
|
|
33
33
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
34
|
+
import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
|
|
34
35
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
35
36
|
import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
|
|
36
37
|
|
|
37
38
|
/** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
|
|
38
39
|
const LINT_TARGETS = ['npm/rules']
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
-
* Друкує підказку зі встановлення `opa` (потрібен для `opa check --strict` і VS Code LSP).
|
|
42
|
-
* @returns {void}
|
|
43
|
-
*/
|
|
44
|
-
function printOpaInstallHints() {
|
|
45
|
-
process.stderr.write(
|
|
46
|
-
[
|
|
47
|
-
'❌ opa не знайдено в PATH.',
|
|
48
|
-
' Без нього не запускається `opa check --strict` (типи + мертвий код у *.rego),',
|
|
49
|
-
' і не працює VS Code-розширення `tsandall.opa` (LSP, format-on-save через opa fmt).',
|
|
50
|
-
' Встанови:',
|
|
51
|
-
' macOS: brew install opa',
|
|
52
|
-
' Universal: https://www.openpolicyagent.org/docs/latest/#1-download-opa',
|
|
53
|
-
''
|
|
54
|
-
].join('\n')
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Друкує підказку зі встановлення `regal`.
|
|
60
|
-
* @returns {void}
|
|
61
|
-
*/
|
|
62
|
-
function printRegalInstallHints() {
|
|
63
|
-
process.stderr.write(
|
|
64
|
-
[
|
|
65
|
-
'❌ regal не знайдено в PATH.',
|
|
66
|
-
' Без нього не перевіряється rego.v1 синтаксис у *.rego (правило `conftest`).',
|
|
67
|
-
' Встанови:',
|
|
68
|
-
' macOS: brew install regal',
|
|
69
|
-
' Universal: https://docs.styra.com/regal#installation',
|
|
70
|
-
''
|
|
71
|
-
].join('\n')
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
41
|
/**
|
|
76
42
|
* Запускає крок з відображенням команди користувачу. Stdout/stderr передаємо як є
|
|
77
43
|
* (`stdio: 'inherit'`), щоб виглядало як прямий виклик у shell.
|
|
@@ -101,19 +67,8 @@ function runStep(bin, args, cwd) {
|
|
|
101
67
|
*/
|
|
102
68
|
export function runLintRegoSteps(cwd = process.cwd()) {
|
|
103
69
|
const root = resolve(cwd)
|
|
104
|
-
const opa =
|
|
105
|
-
const regal =
|
|
106
|
-
|
|
107
|
-
let preflightOk = true
|
|
108
|
-
if (!opa) {
|
|
109
|
-
printOpaInstallHints()
|
|
110
|
-
preflightOk = false
|
|
111
|
-
}
|
|
112
|
-
if (!regal) {
|
|
113
|
-
printRegalInstallHints()
|
|
114
|
-
preflightOk = false
|
|
115
|
-
}
|
|
116
|
-
if (!preflightOk) return 1
|
|
70
|
+
const opa = ensureTool('opa')
|
|
71
|
+
const regal = ensureTool('regal')
|
|
117
72
|
|
|
118
73
|
const targets = LINT_TARGETS.filter(rel => existsSync(resolve(root, rel)))
|
|
119
74
|
if (targets.length === 0) {
|
package/rules/release/change.mjs
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `n-cursor change` — пише один change-файл `<ws>/.changes
|
|
2
|
+
* `n-cursor change` — пише один change-файл `<ws>/.changes/YYMMDD-HHMM.md`.
|
|
3
|
+
* Якщо файл за ту саму хвилину вже існує, додає `-2`, `-3` тощо.
|
|
3
4
|
* Замінює ручне редагування CHANGELOG у feature-флоу (n-changelog.mdc v3.0).
|
|
4
5
|
*/
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import { join } from 'node:path'
|
|
7
8
|
|
|
8
|
-
import { CHANGES_DIR,
|
|
9
|
+
import { CHANGES_DIR, changeFileName, parseChangeFile, serializeChangeFile } from './lib/change-file.mjs'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} error помилка `writeFile`
|
|
13
|
+
* @returns {boolean} true, якщо файл уже існує
|
|
14
|
+
*/
|
|
15
|
+
function isFileExistsError(error) {
|
|
16
|
+
return error instanceof Error && 'code' in error && error.code === 'EEXIST'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Записує change-файл create-only, додаючи числовий suffix лише при локальній колізії.
|
|
21
|
+
* @param {string} dir абсолютний шлях до `.changes`
|
|
22
|
+
* @param {string} content вміст change-файлу
|
|
23
|
+
* @param {number} timestamp epoch milliseconds
|
|
24
|
+
* @returns {Promise<string>} створене ім'я файла
|
|
25
|
+
*/
|
|
26
|
+
async function writeUniqueChangeFile(dir, content, timestamp) {
|
|
27
|
+
for (let sequence = 1; ; sequence++) {
|
|
28
|
+
const name = changeFileName(timestamp, sequence)
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(join(dir, name), content, { flag: 'wx' })
|
|
31
|
+
return name
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (isFileExistsError(error)) continue
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
9
38
|
|
|
10
39
|
/**
|
|
11
40
|
* @param {object} params параметри
|
|
@@ -14,9 +43,10 @@ import { CHANGES_DIR, newChangeFileName, parseChangeFile, serializeChangeFile }
|
|
|
14
43
|
* @param {string} params.message опис
|
|
15
44
|
* @param {string} [params.ws] workspace (за замовчуванням `.`)
|
|
16
45
|
* @param {string} [params.cwd] корінь
|
|
46
|
+
* @param {number} [params.timestamp] epoch milliseconds для детермінованих тестів
|
|
17
47
|
* @returns {Promise<string>} відносний шлях створеного файлу (від ws)
|
|
18
48
|
*/
|
|
19
|
-
export async function writeChange({ bump, section, message, ws = '.', cwd = process.cwd() }) {
|
|
49
|
+
export async function writeChange({ bump, section, message, ws = '.', cwd = process.cwd(), timestamp = Date.now() }) {
|
|
20
50
|
const description = (message ?? '').trim()
|
|
21
51
|
const content = serializeChangeFile({ bump, section, description })
|
|
22
52
|
// Валідація полів: parseChangeFile кидає зрозумілу помилку на невалідних bump/section/порожньому описі.
|
|
@@ -24,8 +54,7 @@ export async function writeChange({ bump, section, message, ws = '.', cwd = proc
|
|
|
24
54
|
|
|
25
55
|
const dir = join(cwd, ws, CHANGES_DIR)
|
|
26
56
|
await mkdir(dir, { recursive: true })
|
|
27
|
-
const name =
|
|
28
|
-
await writeFile(join(dir, name), content)
|
|
57
|
+
const name = await writeUniqueChangeFile(dir, content, timestamp)
|
|
29
58
|
return join(CHANGES_DIR, name)
|
|
30
59
|
}
|
|
31
60
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Один change-файл `<ws>/.changes
|
|
2
|
+
* Один change-файл `<ws>/.changes/YYMMDD-HHMM.md`: YAML-подібний frontmatter
|
|
3
3
|
* із двома ключами (`bump`, `section`) + текст опису. Парсер мінімальний — лише ці два
|
|
4
|
-
* ключі, без зовнішніх залежностей.
|
|
4
|
+
* ключі, без зовнішніх залежностей. Якщо файл за ту саму хвилину вже існує, writer додає
|
|
5
|
+
* числовий suffix (`-2`, `-3`) атомарним create-only записом.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { randomBytes } from 'node:crypto'
|
|
8
8
|
import { existsSync } from 'node:fs'
|
|
9
9
|
import { readdir, readFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
@@ -65,21 +65,36 @@ export function serializeChangeFile(entry) {
|
|
|
65
65
|
export const CHANGES_DIR = '.changes'
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* @param {number} timestamp
|
|
69
|
-
* @
|
|
70
|
-
* @returns {string} `<timestamp>-<suffix>.md`
|
|
68
|
+
* @param {number} timestamp epoch milliseconds
|
|
69
|
+
* @returns {string} local timestamp prefix `YYMMDD-HHMM`
|
|
71
70
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
function formatChangeTimestamp(timestamp) {
|
|
72
|
+
const d = new Date(timestamp)
|
|
73
|
+
const yy = String(d.getFullYear()).slice(-2)
|
|
74
|
+
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
75
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
76
|
+
const hour = String(d.getHours()).padStart(2, '0')
|
|
77
|
+
const minute = String(d.getMinutes()).padStart(2, '0')
|
|
78
|
+
return `${yy}${month}${day}-${hour}${minute}`
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
82
|
+
* @param {number} timestamp epoch milliseconds
|
|
83
|
+
* @param {number} [sequence] collision sequence; `1`/omitted has no suffix
|
|
84
|
+
* @returns {string} `YYMMDD-HHMM.md` or `YYMMDD-HHMM-<n>.md`
|
|
85
|
+
*/
|
|
86
|
+
export function changeFileName(timestamp, sequence = 1) {
|
|
87
|
+
const base = formatChangeTimestamp(timestamp)
|
|
88
|
+
return sequence > 1 ? `${base}-${sequence}.md` : `${base}.md`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Базове ім'я для нового change-файлу. Унікальність забезпечує writer: він спершу
|
|
93
|
+
* пробує `YYMMDD-HHMM.md`, а suffix додає лише при локальному `EEXIST`.
|
|
79
94
|
* @returns {string} результат
|
|
80
95
|
*/
|
|
81
96
|
export function newChangeFileName() {
|
|
82
|
-
return changeFileName(Date.now()
|
|
97
|
+
return changeFileName(Date.now())
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|