@nitra/cursor 2.0.0 → 3.1.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/CHANGELOG.md +30 -0
- package/bin/n-cursor.js +16 -0
- package/package.json +1 -1
- package/rules/docker/docker.mdc +39 -1
- package/rules/docker/js/lint.mjs +4 -0
- package/rules/docker/lib/docker-nginx-user.mjs +122 -0
- package/rules/flow/meta.json +1 -1
- package/rules/js-lint-ci/meta.json +1 -1
- package/rules/vue/js/packages.mjs +87 -30
- package/rules/vue/vue.mdc +3 -1
- package/scripts/lib/worktree-notice.mjs +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.1.0] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- docker: для фінального nginx-unprivileged stage — окрема non-root-перевірка (lib/docker-nginx-user.mjs): жодного USER root/switch-back USER 101|nginx, COPY/ADD лише з --chown=nginx:nginx; gzip під дефолтним uid=101
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- worktree-only skills: банер у SKILL.md став жорстким fail-fast preflight (Крок 0, STOP/ABORT) замість поради; CLAUDE.md отримав секцію-правило про worktree:true скіли
|
|
12
|
+
- vue: увесь стек auto-import (заборона value-імпортів з vue, вимога 'vue' у AutoImport.imports та наявності VueMacros/AutoImport у vite.config) не застосовується до бібліотек компонентів — пакетів із vue у peerDependencies (їхні джерела не проходять через unplugin-auto-import споживача); інші перевірки (esbuild, npm_lifecycle_event) лишаються; додано isVueComponentLibraryPkg
|
|
13
|
+
|
|
14
|
+
## [3.0.0] - 2026-06-01
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- n-cursor flow (v2.0-a Ф0-Ф1.1): каркас dispatcher + CLI `case 'flow'` (init/verify/release/run/resume/cancel/repair — поки stub-и), Capability Router з явною декларацією моделі (native/polyfill, default→polyfill лише за наявного runner-а), crash-safe state-store (.flow.json sibling, atomic temp+fsync+rename, fail-closed на corruption). 29 unit-тестів (withTmpDir).
|
|
19
|
+
- n-cursor flow v2.0-a Ф1.2-Ф1.4: WAL-журнал .events.jsonl (append-only, торований останній рядок толерується), per-branch lock через reuse withLock із fail-closed override (додано опцію onWaitTimeout у спільний with-lock, back-compat), cleanupFlowSiblings (.flow.json/.events.jsonl/lock). recordTransition (WAL: подія до зміни статусу). +22 тести.
|
|
20
|
+
- n-cursor flow v2.0-a Ф2 (verify): reviewer.mjs — Level-1 «Суддя» проганяє lint+coverage gates через ін'єктований runner (fail-fast, fingerprint на повному pass через reuse worktree-fingerprint); flow verify — Пасивний Турнікет: запускає gates у поточному worktree, записує результати+fingerprint у наявний стан (recordTransition), exit 0/1. +11 тестів.
|
|
21
|
+
- n-cursor flow v2.0-a Ф2·2: flow init (n-cursor worktree add + detect-existing-isolation через git rev-parse, init .flow.json з base_commit; reuse worktreePaths/sanitizeBranch) + flow release (n-cursor change + completion snapshot у стан і task record) + snapshot.mjs (buildCompletionSnapshot/upsertSummaryBlock/writeSummaryToTaskRecord). reviewer тепер захоплює вивід проваленого gate. Пасивний Турнікет (init/verify/release) завершено. +18 тестів.
|
|
22
|
+
- n-cursor flow Ф2·4: bundled-правило flow (матеріалізується як .cursor/rules/n-flow.mdc) — контракт Пасивного Турнікета для IDE-агентів (Cursor/Claude Code): init -> сам пишеш код (TDD) -> verify (3 спроби на фейл) -> release. alwaysApply; pure-doc + стандартний fix.mjs (runStandardRule). Авто-дискавериться, активується при sync. Завершує Фасад A повністю.
|
|
23
|
+
- n-cursor flow v2.0-a Ф3 (двигун-блоки): SubagentRunner (§15.1) — selectBackend (sdk>claude>cursor за ANTHROPIC_API_KEY/PATH, fail коли нічого нема), cliRunner (claude/cursor-agent -p, CLI-auth), sdkRunner (claude-agent-sdk, dynamic import); planner — parsePlan (валідація+нормалізація, fail-closed на невалідному) + generatePlan. Усе з мок-ін'єкцією (нуль реальних API/SDK у тестах). +24 тести.
|
|
24
|
+
- n-cursor flow v2.0-a Ф4·a: executor.mjs — серце Активного Раннера. Виконує план покроково: мікропромпт зі стану (не історія) -> спавн субагента -> verify -> commit ЛИШЕ після зеленого (commit-інваріант §4.1.7) -> repair (per-step retry) -> на вичерпанні HITL (blocked-on-human + structured question). microprompt/patchStep — pure. Усе з ін'єкцією runner/verify/commit (нуль реальних LLM/git у тестах). +6 тестів.
|
|
25
|
+
- n-cursor flow v2.0-a Ф4·b: Активний Раннер end-to-end. flow run (ensureWorktree -> planner -> executor; exit 0 done / 1 fail / 2 blocked-on-human), flow resume (safe-resume git reset + HITL-відповіді як hint + свіжі спроби), flow cancel (cleanup sibling-ів), flow repair (--discard-step-work / діагностика). ensureWorktree витягнуто як спільне для init/run. Усі 7 підкоманд реальні. +12 тестів (103 у dispatcher). v2.0-a (Фасади A+B) функціонально завершено.
|
|
26
|
+
- n-cursor flow v2.0-a: (1) budget guard для flow run --autonomous (withBudget обгортає runner, BudgetExceeded при перевищенні maxApiCalls з .n-cursor.json flow.autonomous; на abort -> status failed, exit 1). (2) Ф1.4 борг закрито: worktree remove тепер reuse-кличе cleanupFlowSiblings (flow-sibling-и не осиротіють). Заодно виправлено передіснуючі switch-case-braces у worktree-cli. +4 тести.
|
|
27
|
+
- n-cursor flow v2.0-b: команда n-cursor trace — наскрізна простежуваність (§5.4/§7). Читає front-matter артефактів у docs/{tasks,specs,plans,adr}, будує ланцюг за лінками adr/spec/plan/change/task, флагує розриви (лінк на неіснуючий файл) з exit 1; --json для machine-readable. parseFrontMatter/analyze/render — pure, FS ін'єктовний. Підтверджено на власних spec<->plan. +10 тестів.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- n-cursor flow v2.0.0 (major): Dual-Mode Dispatcher — Пасивний Турнікет (flow init/verify/release) + Активний Раннер (flow run/resume/cancel/repair) + n-cursor trace + docs/{specs,plans} міграція
|
|
32
|
+
|
|
3
33
|
## [2.0.0] - 2026-05-31
|
|
4
34
|
|
|
5
35
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -641,6 +641,21 @@ function buildClaudeLintParallelismSectionLines() {
|
|
|
641
641
|
]
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Рендерить секцію для CLAUDE.md: скіли з `meta.json.worktree === true` запускаються
|
|
646
|
+
* лише в окремому git-worktree (дублює fail-fast банер `SKILL.md` як завжди-читане правило).
|
|
647
|
+
* @returns {string[]} рядки для вставки (з порожнім рядком на початку)
|
|
648
|
+
*/
|
|
649
|
+
function buildClaudeWorktreeEnforcementSectionLines() {
|
|
650
|
+
return [
|
|
651
|
+
'',
|
|
652
|
+
'## Worktree-only skills (`meta.json` → `worktree: true`)',
|
|
653
|
+
'',
|
|
654
|
+
'Скіл із **`worktree: true`** у `meta.json` запускається **виключно** в окремому git-worktree (`.worktrees/<branch>/`) — **не** в основному дереві й **не** паралельно. Перший крок такого скіла (блок `n-cursor:worktree:start` у його `SKILL.md`) — **preflight**: якщо `git rev-parse --show-toplevel` не вказує під `.worktrees/`, **STOP** і спершу `npx @nitra/cursor worktree add <branch> "<навіщо>"` → `cd .worktrees/<branch>`. Чисте робоче дерево — **не** привід пропустити preflight.',
|
|
655
|
+
''
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
|
|
644
659
|
/**
|
|
645
660
|
* Рендерить секцію Skills для CLAUDE.md з урахуванням наявних slash-команд.
|
|
646
661
|
* @returns {Promise<string[]>} готові рядки секції (або порожній масив)
|
|
@@ -701,6 +716,7 @@ async function syncClaudeMd(ignore) {
|
|
|
701
716
|
}
|
|
702
717
|
|
|
703
718
|
lines.push(...buildClaudeLintParallelismSectionLines())
|
|
719
|
+
lines.push(...buildClaudeWorktreeEnforcementSectionLines())
|
|
704
720
|
|
|
705
721
|
const skillsSectionLines = await buildClaudeSkillsSectionLines()
|
|
706
722
|
lines.push(...skillsSectionLines)
|
package/package.json
CHANGED
package/rules/docker/docker.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Dockerfile — lint-docker / hadolint; перевірка check-docker
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.10'
|
|
4
4
|
globs: "**/Dockerfile*"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -88,6 +88,44 @@ CMD ["./app"]
|
|
|
88
88
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### nginx-unprivileged — без USER, із --chown
|
|
92
|
+
|
|
93
|
+
Окрема гілка для фронтенду на базі **`nginxinc/nginx-unprivileged`** (будь-який тег, з/без `mirror.gcr.io/`-префікса). Цей образ **уже** оголошує `USER 101` і `EXPOSE 8080`, тож у фінальному stage **не потрібні** жодні явні `USER`-інструкції:
|
|
94
|
+
|
|
95
|
+
- **жодного `USER root` / `USER 0`** для білд-кроків: він перезатирає успадкований `USER 101`, і якщо потім не повернути non-root — фінальний образ лишається root, а k8s із `runAsNonRoot: true` падає з `CreateContainerConfigError`;
|
|
96
|
+
- **жодного switch-back** `USER 101` / `USER nginx` наприкінці stage — це лише симптом зайвого `USER root` на початку (повертати треба саме **числовим** UID, бо kubelet не підтверджує non-root за іменем `nginx`). Канон — взагалі не виходити з-під дефолтного 101;
|
|
97
|
+
- **`COPY`/`ADD` лише з `--chown`** (канон — `--chown=nginx:nginx`): без нього файли копіюються власником root і дефолтний non-root користувач (uid=101) не зможе читати статику.
|
|
98
|
+
|
|
99
|
+
Build-stage не чіпаємо — там root і tooling норма. Перевіряє **`npm/rules/docker/lib/docker-nginx-user.mjs`** (підключено в **`npm/rules/docker/js/lint.mjs`**, точка входу обходу — **`npm/rules/docker/fix.mjs`**).
|
|
100
|
+
|
|
101
|
+
#### Антипатерн (це правило ловить)
|
|
102
|
+
|
|
103
|
+
```dockerfile
|
|
104
|
+
FROM mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim
|
|
105
|
+
USER root
|
|
106
|
+
COPY ./k8s/nginx.conf /etc/nginx/conf.d/default.conf
|
|
107
|
+
COPY --from=build /app/dist ./
|
|
108
|
+
RUN find ./ -type f -name "*.js" -exec gzip -k {} \;
|
|
109
|
+
USER 101 # повернення назад — симптом того, що був зайвий USER root
|
|
110
|
+
EXPOSE 8080
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Канон (привести до цього)
|
|
114
|
+
|
|
115
|
+
```dockerfile
|
|
116
|
+
FROM mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim
|
|
117
|
+
|
|
118
|
+
COPY --chown=nginx:nginx ./k8s/nginx.conf /etc/nginx/conf.d/default.conf
|
|
119
|
+
|
|
120
|
+
WORKDIR /usr/share/nginx/html
|
|
121
|
+
|
|
122
|
+
COPY --from=build --chown=nginx:nginx /app/dist ./
|
|
123
|
+
|
|
124
|
+
RUN find ./ -type f -name "*.js" -exec gzip -k {} \;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
(без жодного `USER`, gzip під дефолтним користувачем 101; `EXPOSE 8080` теж зайвий — база вже його оголошує)
|
|
128
|
+
|
|
91
129
|
## Область
|
|
92
130
|
|
|
93
131
|
- Усі файли з іменем **`Dockerfile`** або **`Dockerfile.*`** (наприклад `Dockerfile.prod`) у репозиторії, крім ігнорованих каталогів (`node_modules`, `.git`, `dist`, …) — як у **`rules/docker/fix.mjs`**.
|
package/rules/docker/js/lint.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
31
31
|
import { basename } from 'node:path'
|
|
32
32
|
|
|
33
33
|
import { getMirrorGcrHint, getFromImageToken } from '../lib/docker-mirror.mjs'
|
|
34
|
+
import { getNginxUnprivilegedUserHint } from '../lib/docker-nginx-user.mjs'
|
|
34
35
|
import { lintDockerfileWithHadolint, posixRel } from '../lib/docker-hadolint.mjs'
|
|
35
36
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
36
37
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
@@ -336,6 +337,9 @@ async function checkDockerfile(reporter, root, abs) {
|
|
|
336
337
|
const nginxSlimHint = getNginxAlpineSlimTagHint(content)
|
|
337
338
|
if (nginxSlimHint) fail(`${rel} (nginx tag): ${nginxSlimHint}`)
|
|
338
339
|
|
|
340
|
+
const nginxUserHint = getNginxUnprivilegedUserHint(content)
|
|
341
|
+
if (nginxUserHint) fail(`${rel} (nginx non-root): ${nginxUserHint}`)
|
|
342
|
+
|
|
339
343
|
const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
|
|
340
344
|
const tail = (stdout + stderr).trim()
|
|
341
345
|
if (ok) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевірка для фінального (runtime) stage на базі `nginxinc/nginx-unprivileged`.
|
|
3
|
+
*
|
|
4
|
+
* Образ `nginx-unprivileged` уже оголошує `USER 101` і `EXPOSE 8080`, тож у Dockerfile
|
|
5
|
+
* **не повинно** бути жодних явних `USER`-інструкцій у цьому stage:
|
|
6
|
+
*
|
|
7
|
+
* - `USER root` (або `USER 0`) для білд-кроків перезатирає успадкований `USER 101`; якщо потім
|
|
8
|
+
* не повернути non-root — фінальний образ лишається root, і k8s із `runAsNonRoot: true` падає з
|
|
9
|
+
* `CreateContainerConfigError`. Повертати треба саме **числовим** UID (`USER 101`), бо kubelet не
|
|
10
|
+
* підтверджує non-root за іменем `nginx` — тож повернення `USER 101`/`USER nginx` у кінці stage
|
|
11
|
+
* є симптомом зайвого `USER root` на початку.
|
|
12
|
+
* - Найбезпечніший канон — взагалі не виходити з-під дефолтного 101: ні `USER root`, ні
|
|
13
|
+
* switch-back. Будь-який явний `USER` у такому stage прапорцюється як зайвий.
|
|
14
|
+
*
|
|
15
|
+
* Крім того, `COPY`/`ADD` без `--chown` копіює файли власником root — їх не зможе читати дефолтний
|
|
16
|
+
* non-root користувач (uid=101); тому в цьому stage кожен `COPY`/`ADD` має мати `--chown` (канон —
|
|
17
|
+
* `--chown=nginx:nginx`).
|
|
18
|
+
*
|
|
19
|
+
* Це окрема гілка від генеричного non-root-правила (`addgroup/adduser` + `USER app` для alpine-бекендів,
|
|
20
|
+
* див. `getNonRootRuntimeHint` у `../js/lint.mjs`): для nginx канон — навпаки, **відсутність** `USER`.
|
|
21
|
+
*
|
|
22
|
+
* Тригер — лише фінальний `FROM`, що базується на `nginxinc/nginx-unprivileged` (з урахуванням
|
|
23
|
+
* `mirror.gcr.io/…`-префікса й будь-якого тега). Build-stage-и не чіпаємо — там root і tooling норма.
|
|
24
|
+
*
|
|
25
|
+
* Взірець структури base-image-специфічного чек-модуля — сусідній `./docker-mirror.mjs`.
|
|
26
|
+
*/
|
|
27
|
+
import { getFromImageToken } from './docker-mirror.mjs'
|
|
28
|
+
|
|
29
|
+
const NEWLINE_RE = /\r?\n/
|
|
30
|
+
const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
|
|
31
|
+
const COPY_ADD_RE = /^\s*(COPY|ADD)\b(.*)$/iu
|
|
32
|
+
const CHOWN_FLAG_RE = /(?:^|\s)--chown=/iu
|
|
33
|
+
|
|
34
|
+
/** Шлях репозиторію nginx-unprivileged (після зняття `mirror.gcr.io/`/`docker.io/`-префікса й тега/digest). */
|
|
35
|
+
const NGINX_UNPRIVILEGED_REPO_RE = /(?:^|\/)nginxinc\/nginx-unprivileged(?::|@|$)/iu
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Чи базується ref `FROM` на образі `nginxinc/nginx-unprivileged` (будь-який тег, з/без `mirror.gcr.io/`).
|
|
39
|
+
* @param {string} image — токен образу після `FROM`
|
|
40
|
+
* @returns {boolean} true, якщо це nginx-unprivileged
|
|
41
|
+
*/
|
|
42
|
+
export function isNginxUnprivilegedImage(image) {
|
|
43
|
+
return NGINX_UNPRIVILEGED_REPO_RE.test((image || '').trim())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {{ image: string, lines: Array<{ lineNo: number, text: string }> }} FinalStage
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Виділяє фінальний (останній `FROM` … кінець файла) stage з номерами рядків.
|
|
52
|
+
* @param {string} fileContent — вміст Dockerfile/Containerfile
|
|
53
|
+
* @returns {FinalStage | null} фінальний stage або null, якщо `FROM` немає
|
|
54
|
+
*/
|
|
55
|
+
function getFinalStage(fileContent) {
|
|
56
|
+
const lines = fileContent.split(NEWLINE_RE)
|
|
57
|
+
/** @type {{ image: string, idx: number } | null} */
|
|
58
|
+
let lastFrom = null
|
|
59
|
+
for (const [idx, line] of lines.entries()) {
|
|
60
|
+
const image = getFromImageToken(line)
|
|
61
|
+
if (image) lastFrom = { image, idx }
|
|
62
|
+
}
|
|
63
|
+
if (!lastFrom) return null
|
|
64
|
+
const stageLines = lines.slice(lastFrom.idx).map((text, i) => ({ lineNo: lastFrom.idx + i + 1, text }))
|
|
65
|
+
return { image: lastFrom.image, lines: stageLines }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Нормалізує токен `USER` (без лапок, lower-case, без зайвих пробілів).
|
|
70
|
+
* @param {string} token — захоплений токен після `USER`
|
|
71
|
+
* @returns {string} нормалізований токен
|
|
72
|
+
*/
|
|
73
|
+
function normalizeUserToken(token) {
|
|
74
|
+
return token.replaceAll('"', '').replaceAll("'", '').trim().toLowerCase()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Перевіряє фінальний nginx-unprivileged stage на зайві `USER` і `COPY`/`ADD` без `--chown`.
|
|
79
|
+
*
|
|
80
|
+
* Збирає всі порушення в один рядок (по одному пункту на рядок) — щоб один прогін показав і
|
|
81
|
+
* `USER root`, і switch-back `USER 101`, і `COPY` без `--chown` одразу.
|
|
82
|
+
* @param {string} fileContent — вміст Dockerfile/Containerfile
|
|
83
|
+
* @returns {string | null} повідомлення помилки або null, якщо порушень немає / це не nginx-stage
|
|
84
|
+
*/
|
|
85
|
+
export function getNginxUnprivilegedUserHint(fileContent) {
|
|
86
|
+
const stage = getFinalStage(fileContent)
|
|
87
|
+
if (!stage) return null
|
|
88
|
+
if (!isNginxUnprivilegedImage(stage.image)) return null
|
|
89
|
+
|
|
90
|
+
/** @type {string[]} */
|
|
91
|
+
const problems = []
|
|
92
|
+
// Перший рядок stage — це сам FROM; USER/COPY у ньому неможливі, але цикл лишаємо загальним.
|
|
93
|
+
for (const { lineNo, text } of stage.lines) {
|
|
94
|
+
const u = text.match(USER_LINE_RE)
|
|
95
|
+
if (u) {
|
|
96
|
+
const token = normalizeUserToken(u[1])
|
|
97
|
+
if (token === 'root' || token === '0') {
|
|
98
|
+
problems.push(
|
|
99
|
+
`рядок ${lineNo}: прибери \`USER ${u[1]}\` — у nginx-unprivileged не можна перемикатися на root (інакше фінальний образ лишиться root і k8s із runAsNonRoot впаде)`
|
|
100
|
+
)
|
|
101
|
+
} else if (token === '101' || token === 'nginx') {
|
|
102
|
+
problems.push(
|
|
103
|
+
`рядок ${lineNo}: прибери зайвий \`USER ${u[1]}\` — база nginx-unprivileged вже працює від uid=101 (повернення USER назад — симптом зайвого USER root)`
|
|
104
|
+
)
|
|
105
|
+
} else {
|
|
106
|
+
problems.push(
|
|
107
|
+
`рядок ${lineNo}: прибери явний \`USER ${u[1]}\` — база nginx-unprivileged вже працює від non-root (uid=101), окремий USER не потрібен`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
const c = text.match(COPY_ADD_RE)
|
|
113
|
+
if (c && !CHOWN_FLAG_RE.test(text)) {
|
|
114
|
+
problems.push(
|
|
115
|
+
`рядок ${lineNo}: додай \`--chown=nginx:nginx\` до \`${c[1].toUpperCase()}\` — статику має читати non-root користувач (uid=101)`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (problems.length === 0) return null
|
|
121
|
+
return problems.join('\n - ')
|
|
122
|
+
}
|
package/rules/flow/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{}
|
|
1
|
+
{ "auto": "завжди" }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "lint": "ci" }
|
|
1
|
+
{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "ci" }
|
|
@@ -226,6 +226,20 @@ async function checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail,
|
|
|
226
226
|
passFn(`${prefix}jsconfig.json присутній`)
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Чи є пакет бібліотекою компонентів Vue — `vue` оголошено в `peerDependencies`.
|
|
231
|
+
*
|
|
232
|
+
* Такі пакети споживаються Vite-проєктами як залежність; їхні власні джерела **не** проходять
|
|
233
|
+
* через `unplugin-auto-import` споживача (auto-import резолвиться лише в коді самого додатка, не в
|
|
234
|
+
* `node_modules`). Тому в бібліотеці компонентів явні `import { … } from 'vue'` обовʼязкові, і правило
|
|
235
|
+
* авто-імпорту (заборона value-імпортів з `'vue'`) до неї **не** застосовується.
|
|
236
|
+
* @param {{ peerDependencies?: Record<string, string> }} pkg розпарсений package.json
|
|
237
|
+
* @returns {boolean} true, якщо `vue` присутній у `peerDependencies`
|
|
238
|
+
*/
|
|
239
|
+
export function isVueComponentLibraryPkg(pkg) {
|
|
240
|
+
return Boolean(pkg?.peerDependencies?.vue)
|
|
241
|
+
}
|
|
242
|
+
|
|
229
243
|
/**
|
|
230
244
|
* Витягує текст аргументів першого виклику `AutoImport(` з vite.config зі збалансованими дужками.
|
|
231
245
|
* Повертає `null`, якщо виклик не знайдено або дужки не збалансовані (тоді перевірка `'vue'`
|
|
@@ -266,13 +280,14 @@ function viteConfigHasVueInAutoImports(content) {
|
|
|
266
280
|
/**
|
|
267
281
|
* Перевіряє vite.config на наявність VueMacros і AutoImport.
|
|
268
282
|
* @param {string} rootDir параметр rootDir
|
|
283
|
+
* @param {boolean} isComponentLibrary чи це бібліотека компонентів (vue у peerDependencies) — тоді auto-import не застосовується
|
|
269
284
|
* @param {string} prefix параметр prefix
|
|
270
285
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
271
286
|
* @param {(msg: string) => void} fail callback при помилці
|
|
272
287
|
* @returns {Promise<{ hasVueAutoImport: boolean }>} ознака успішно сконфігурованого vue-auto-import (для checkVueImportViolations)
|
|
273
288
|
* @param {string} cwd корінь репозиторію
|
|
274
289
|
*/
|
|
275
|
-
async function checkViteConfig(rootDir, prefix, passFn, fail, cwd) {
|
|
290
|
+
async function checkViteConfig(rootDir, isComponentLibrary, prefix, passFn, fail, cwd) {
|
|
276
291
|
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
277
292
|
const viteConfig = configFiles.find(f => existsSync(join(cwd, rootDir, f)))
|
|
278
293
|
if (!viteConfig) {
|
|
@@ -283,27 +298,36 @@ async function checkViteConfig(rootDir, prefix, passFn, fail, cwd) {
|
|
|
283
298
|
if (ESBUILD_RE.test(content)) {
|
|
284
299
|
fail(`${prefix}${viteConfig} містить 'esbuild' — заміни на 'rolldown'`)
|
|
285
300
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
// VueMacros + AutoImport (і 'vue' у його imports) — інструментарій auto-import Vite-додатка;
|
|
302
|
+
// бібліотека компонентів (vue у peerDependencies) споживається готовою і не потребує цього стеку.
|
|
303
|
+
// npm_lifecycle_event (Bun-compat) перевіряємо нижче незалежно — це не auto-import.
|
|
304
|
+
const hasVueAutoImport = viteConfigHasVueInAutoImports(content)
|
|
305
|
+
if (isComponentLibrary) {
|
|
306
|
+
passFn(
|
|
307
|
+
`${prefix}${viteConfig}: бібліотека компонентів (vue у peerDependencies) — VueMacros/AutoImport не вимагаються`
|
|
308
|
+
)
|
|
309
|
+
} else {
|
|
310
|
+
const checks = [
|
|
311
|
+
{ token: 'VueMacros', ok: `${viteConfig} використовує VueMacros`, err: `${viteConfig} не містить VueMacros` },
|
|
312
|
+
{ token: 'AutoImport', ok: `${viteConfig} використовує AutoImport`, err: `${viteConfig} не містить AutoImport` }
|
|
313
|
+
]
|
|
314
|
+
for (const { token, ok, err } of checks) {
|
|
315
|
+
if (content.includes(token)) {
|
|
316
|
+
passFn(`${prefix}${ok}`)
|
|
317
|
+
} else {
|
|
318
|
+
fail(`${prefix}${err}`)
|
|
319
|
+
}
|
|
295
320
|
}
|
|
296
|
-
}
|
|
297
321
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
322
|
+
if (content.includes('AutoImport(')) {
|
|
323
|
+
if (hasVueAutoImport) {
|
|
324
|
+
passFn(`${prefix}${viteConfig}: AutoImport({ imports: [..., 'vue', ...] }) — value-імпорти з 'vue' покриті`)
|
|
325
|
+
} else {
|
|
326
|
+
fail(
|
|
327
|
+
`${prefix}${viteConfig}: AutoImport не містить 'vue' у imports — додай 'vue' (інакше прибирати ` +
|
|
328
|
+
`value-імпорти на кшталт \`import { ref } from 'vue'\` небезпечно: ref/createApp тощо нікому буде надати)`
|
|
329
|
+
)
|
|
330
|
+
}
|
|
307
331
|
}
|
|
308
332
|
}
|
|
309
333
|
|
|
@@ -367,12 +391,29 @@ async function checkVueNodeImportViolations(rootDir, absPackageRoot, ignorePaths
|
|
|
367
391
|
* @param {string} rootDir відносний шлях до пакета
|
|
368
392
|
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
369
393
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
394
|
+
* @param {boolean} isComponentLibrary чи це бібліотека компонентів (vue у peerDependencies) — її джерела не проходять auto-import
|
|
370
395
|
* @param {boolean} hasVueAutoImport чи `AutoImport({ imports: [..., 'vue', ...] })` сконфігуровано
|
|
371
396
|
* @param {string} prefix префікс повідомлення `[<pkg>] `
|
|
372
397
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
373
398
|
* @param {(msg: string) => void} fail callback при помилці
|
|
374
399
|
*/
|
|
375
|
-
async function checkVueImportViolations(
|
|
400
|
+
async function checkVueImportViolations(
|
|
401
|
+
rootDir,
|
|
402
|
+
absPackageRoot,
|
|
403
|
+
ignorePaths,
|
|
404
|
+
isComponentLibrary,
|
|
405
|
+
hasVueAutoImport,
|
|
406
|
+
prefix,
|
|
407
|
+
passFn,
|
|
408
|
+
fail
|
|
409
|
+
) {
|
|
410
|
+
if (isComponentLibrary) {
|
|
411
|
+
passFn(
|
|
412
|
+
`${prefix}бібліотека компонентів (vue у peerDependencies) — явні value-імпорти з 'vue' дозволені ` +
|
|
413
|
+
`(джерела не проходять через unplugin-auto-import споживача)`
|
|
414
|
+
)
|
|
415
|
+
return
|
|
416
|
+
}
|
|
376
417
|
if (!hasVueAutoImport) {
|
|
377
418
|
passFn(`${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`)
|
|
378
419
|
return
|
|
@@ -409,38 +450,54 @@ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, ha
|
|
|
409
450
|
/**
|
|
410
451
|
* Перевіряє залежності та vite.config одного Vue-пакета.
|
|
411
452
|
* @param {string} rootDir відносний шлях до пакета
|
|
453
|
+
* @param {boolean} isComponentLibrary чи це бібліотека компонентів (vue у peerDependencies) — auto-import не застосовується
|
|
412
454
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
413
455
|
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
414
456
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
415
457
|
* @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
|
|
416
458
|
* @param {string} cwd корінь репозиторію
|
|
417
459
|
*/
|
|
418
|
-
async function checkVuePackage(rootDir, ignorePaths, fail, passFn, cwd) {
|
|
460
|
+
async function checkVuePackage(rootDir, isComponentLibrary, ignorePaths, fail, passFn, cwd) {
|
|
419
461
|
const prefix = `[${packageLabel(rootDir)}] `
|
|
420
462
|
passFn(`${prefix}package.json залежності перевіряє npx @nitra/cursor fix → vue.package_json`)
|
|
421
463
|
|
|
422
464
|
await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail, cwd)
|
|
423
465
|
|
|
424
|
-
const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail, cwd)
|
|
425
|
-
await checkVueImportViolations(
|
|
466
|
+
const { hasVueAutoImport } = await checkViteConfig(rootDir, isComponentLibrary, prefix, passFn, fail, cwd)
|
|
467
|
+
await checkVueImportViolations(
|
|
468
|
+
rootDir,
|
|
469
|
+
join(cwd, rootDir),
|
|
470
|
+
ignorePaths,
|
|
471
|
+
isComponentLibrary,
|
|
472
|
+
hasVueAutoImport,
|
|
473
|
+
prefix,
|
|
474
|
+
passFn,
|
|
475
|
+
fail
|
|
476
|
+
)
|
|
426
477
|
await checkVueNodeImportViolations(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
|
|
427
478
|
await checkEsbuildMentions(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
|
|
428
479
|
}
|
|
429
480
|
|
|
430
481
|
/**
|
|
431
|
-
* Збирає корені пакетів, у яких у `dependencies` є `vue
|
|
482
|
+
* Збирає корені пакетів, у яких у `dependencies` є `vue`, із ознакою «бібліотека компонентів».
|
|
483
|
+
*
|
|
484
|
+
* Пакети, де `vue` лише в `peerDependencies` (без `dependencies`), — це бібліотеки компонентів, які
|
|
485
|
+
* споживаються Vite-додатками; вони не є самостійними Vite-проєктами, тож app-перевірки (vite-env,
|
|
486
|
+
* VueMacros тощо) до них не застосовуються — їх не збираємо. Якщо ж пакет має `vue` і в
|
|
487
|
+
* `dependencies` (повноцінний проєкт), і в `peerDependencies` — позначаємо `isComponentLibrary`,
|
|
488
|
+
* щоб вимкнути саме правило auto-import (його джерела не проходять через unplugin-auto-import споживача).
|
|
432
489
|
* @param {string[]} roots усі корені пакетів monorepo
|
|
433
|
-
* @returns {Promise<string
|
|
490
|
+
* @returns {Promise<Array<{ rootDir: string, isComponentLibrary: boolean }>>} пакети з vue у dependencies
|
|
434
491
|
* @param {string} cwd корінь репозиторію
|
|
435
492
|
*/
|
|
436
493
|
async function collectVueRoots(roots, cwd) {
|
|
437
|
-
/** @type {string
|
|
494
|
+
/** @type {Array<{ rootDir: string, isComponentLibrary: boolean }>} */
|
|
438
495
|
const vueRoots = []
|
|
439
496
|
for (const r of roots) {
|
|
440
497
|
const p = join(cwd, r, 'package.json')
|
|
441
498
|
if (!existsSync(p)) continue
|
|
442
499
|
const pkg = JSON.parse(await readFile(p, 'utf8'))
|
|
443
|
-
if (pkg.dependencies?.vue) vueRoots.push(r)
|
|
500
|
+
if (pkg.dependencies?.vue) vueRoots.push({ rootDir: r, isComponentLibrary: isVueComponentLibraryPkg(pkg) })
|
|
444
501
|
}
|
|
445
502
|
return vueRoots
|
|
446
503
|
}
|
|
@@ -487,8 +544,8 @@ export async function check(cwd = process.cwd()) {
|
|
|
487
544
|
await checkVueVolarRecommendation(pass, fail, cwd)
|
|
488
545
|
|
|
489
546
|
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
490
|
-
for (const
|
|
491
|
-
await checkVuePackage(
|
|
547
|
+
for (const { rootDir, isComponentLibrary } of vueRoots) {
|
|
548
|
+
await checkVuePackage(rootDir, isComponentLibrary, ignorePaths, fail, pass, cwd)
|
|
492
549
|
}
|
|
493
550
|
|
|
494
551
|
return reporter.getExitCode()
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Vue
|
|
3
|
-
version: '2.
|
|
3
|
+
version: '2.1'
|
|
4
4
|
globs: "**/*.vue"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -290,6 +290,8 @@ export default defineConfig({
|
|
|
290
290
|
|
|
291
291
|
Потрібно використовувати unplugin-auto-import для автоматичного імпортування компонентів, composables, utils та інших функцій і прибирати з файлів усередині Vite проектів відповідні ручні імпорти, зокрема рядки виду `import { … } from 'vue'` — API Vue (`ref`, `computed`, `watch` тощо) мають підставлятися через auto-import, а не дублюватися явним імпортом з модуля `vue`.
|
|
292
292
|
|
|
293
|
+
**Виняток — бібліотеки компонентів (`vue` у `peerDependencies`).** Увесь стек auto-import застосовується лише коли `vue` підключено як звичайну `dependencies`. Якщо ж пакет оголошує `vue` у `peerDependencies` — це проєкт-бібліотека компонентів: його джерела споживаються Vite-додатками і **не** проходять через `unplugin-auto-import` споживача (auto-import резолвиться лише в коді самого додатка, не в `node_modules`). Тому до таких пакетів **не** застосовуються: заборона явних `import { ref, computed, … } from 'vue'` (вони обовʼязкові), вимога `'vue'` у `AutoImport.imports`, а також вимоги наявності `VueMacros` / `AutoImport` у `vite.config`. Решта перевірок (заборона `esbuild`, `process.env.npm_lifecycle_event` тощо) лишаються. Тригер винятку — `isVueComponentLibraryPkg` у `npm/rules/vue/js/packages.mjs`.
|
|
294
|
+
|
|
293
295
|
Потрібно використовувати vite-plugin-vue-layouts-next для автоматичного імпортування layout компонентів.
|
|
294
296
|
|
|
295
297
|
## npm_lifecycle_event
|
|
@@ -12,9 +12,15 @@ export const WORKTREE_START = '<!-- n-cursor:worktree:start -->'
|
|
|
12
12
|
/** Маркер кінця worktree-блоку. */
|
|
13
13
|
export const WORKTREE_END = '<!-- n-cursor:worktree:end -->'
|
|
14
14
|
|
|
15
|
-
const NOTICE_BODY =
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const NOTICE_BODY = `> [!IMPORTANT]
|
|
16
|
+
> **Worktree-only skill.** Виконується **виключно** в окремому git-worktree (\`.worktrees/<branch>/\`) і **не** паралелиться — один інстанс за раз.
|
|
17
|
+
|
|
18
|
+
**Крок 0 — preflight (обовʼязковий, перед будь-якими іншими діями).** Якщо перевірка падає — **STOP**: створи worktree і лише тоді продовжуй. Не виконуй **жоден** наступний крок скіла, поки preflight не завершився успіхом.
|
|
19
|
+
|
|
20
|
+
\`\`\`bash
|
|
21
|
+
git rev-parse --show-toplevel | grep -q '/\\.worktrees/' \\
|
|
22
|
+
|| { echo "ABORT: не у worktree. Спершу: npx @nitra/cursor worktree add <branch> \\"<навіщо>\\" && cd .worktrees/<branch>"; exit 1; }
|
|
23
|
+
\`\`\``
|
|
18
24
|
|
|
19
25
|
/** Наявний блок разом із сусідніми порожніми рядками (для чистого видалення). */
|
|
20
26
|
const BLOCK_RE = /\n*<!-- n-cursor:worktree:start -->[\s\S]*?<!-- n-cursor:worktree:end -->\n*/u
|