@nitra/cursor 1.8.139 → 1.8.141
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/bin/n-cursor.js +16 -0
- package/package.json +1 -1
- package/scripts/check-docker.mjs +28 -15
- package/scripts/utils/docker-mirror.mjs +3 -2
- package/skills/lint/SKILL.md +21 -0
package/bin/n-cursor.js
CHANGED
|
@@ -551,6 +551,20 @@ async function removeOrphanManagedSkillDirs(skillsRoot, configSkills) {
|
|
|
551
551
|
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Рендерить коротку секцію для CLAUDE.md: не розпаралелювати лінт (ESLint) між shells/субагентами.
|
|
556
|
+
* @returns {string[]} рядки для вставки (з порожнім рядком на початку)
|
|
557
|
+
*/
|
|
558
|
+
function buildClaudeLintParallelismSectionLines() {
|
|
559
|
+
return [
|
|
560
|
+
'',
|
|
561
|
+
'## Лінт і ESLint (без паралельних запусків)',
|
|
562
|
+
'',
|
|
563
|
+
'Щоб не запускати **кілька** одночасних **`eslint`** (і не перевантажувати диск/CPU), **заборонено** стартувати `bun run lint` / `lint-js` / `eslint` **паралельно** в різних Bash-задачах, **фонових** shells чи **субагентах** (Task тощо). Має бути **один** послідовний прогон на сесію; команда **`/n-lint`** — **не** ділити на паралельні підзадачі. Деталі: `.cursor/skills/n-lint/SKILL.md`.',
|
|
564
|
+
'',
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
|
|
554
568
|
/**
|
|
555
569
|
* Рендерить секцію Skills для CLAUDE.md з урахуванням наявних slash-команд.
|
|
556
570
|
* @returns {Promise<string[]>} готові рядки секції (або порожній масив)
|
|
@@ -610,6 +624,8 @@ async function syncClaudeMd(ignore) {
|
|
|
610
624
|
lines.push(`@${RULES_DIR}/${mdcFile}`)
|
|
611
625
|
}
|
|
612
626
|
|
|
627
|
+
lines.push(...buildClaudeLintParallelismSectionLines())
|
|
628
|
+
|
|
613
629
|
const skillsSectionLines = await buildClaudeSkillsSectionLines()
|
|
614
630
|
lines.push(...skillsSectionLines, '')
|
|
615
631
|
const claudeMdPath = join(cwd(), 'CLAUDE.md')
|
package/package.json
CHANGED
package/scripts/check-docker.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
|
|
10
10
|
* містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
|
|
11
11
|
* `mirror.gcr.io/library/python:*`
|
|
12
|
-
* - frontend: `mirror.gcr.io/
|
|
12
|
+
* - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` (preferred) або `mirror.gcr.io/library/nginx:*`,
|
|
13
|
+
* `mirror.gcr.io/openresty/openresty:*`
|
|
13
14
|
*
|
|
14
15
|
* Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
|
|
15
16
|
* то очікується компіляція в один бінарник через `bun build --compile` у build stage, а у
|
|
@@ -18,8 +19,10 @@
|
|
|
18
19
|
* Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
|
|
19
20
|
* дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
|
|
20
21
|
*
|
|
21
|
-
* Для `mirror.gcr.io/library/nginx` у
|
|
22
|
-
* мінімальні образи), не `latest` /
|
|
22
|
+
* Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`, `mirror.gcr.io/library/nginx`) у
|
|
23
|
+
* будь-якому `FROM` очікується тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
|
|
24
|
+
* `alpine` / інші. `nginx-unprivileged` запускається від не-root користувача (uid=101) без явного
|
|
25
|
+
* `USER` у Dockerfile — перевірка non-root для нього пропускається.
|
|
23
26
|
*
|
|
24
27
|
* Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
|
|
25
28
|
* тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
|
|
@@ -40,6 +43,7 @@ const BUN_WORD_RE = /\bbun\b/iu
|
|
|
40
43
|
const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
|
|
41
44
|
|
|
42
45
|
const NGINX_MIRROR_PREFIX = 'mirror.gcr.io/library/nginx'
|
|
46
|
+
const NGINX_UNPRIVILEGED_MIRROR_PREFIX = 'mirror.gcr.io/nginxinc/nginx-unprivileged'
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* @typedef {{
|
|
@@ -94,6 +98,7 @@ const RUNTIME_IMAGES = /** @type {const} */ ([
|
|
|
94
98
|
'mirror.gcr.io/library/php',
|
|
95
99
|
'mirror.gcr.io/library/python',
|
|
96
100
|
'mirror.gcr.io/library/nginx',
|
|
101
|
+
'mirror.gcr.io/nginxinc/nginx-unprivileged',
|
|
97
102
|
'mirror.gcr.io/openresty/openresty'
|
|
98
103
|
])
|
|
99
104
|
|
|
@@ -103,7 +108,7 @@ const DEBIAN_VIA_MIRROR_RE = /^mirror\.gcr\.io\/library\/debian:(.+)$/i
|
|
|
103
108
|
/**
|
|
104
109
|
* Чи ref фінального `FROM` відповідає дозволеним у docker.mdc (multistage / runtime).
|
|
105
110
|
* @param {string} lastLower ref без digest, lower case
|
|
106
|
-
* @returns {boolean}
|
|
111
|
+
* @returns {boolean} true, якщо образ дозволений як фінальний runtime
|
|
107
112
|
*/
|
|
108
113
|
function isAllowedFinalRuntimeImage(lastLower) {
|
|
109
114
|
if (lastLower === 'scratch' || lastLower.startsWith('scratch:')) {
|
|
@@ -189,7 +194,9 @@ export function getBunCompileHint(fileContent) {
|
|
|
189
194
|
const hasBunInstall = BUN_INSTALL_RE.test(fileContent)
|
|
190
195
|
const isFinalAlpine = lastLower.startsWith('mirror.gcr.io/library/alpine:')
|
|
191
196
|
const isFinalFrontend =
|
|
192
|
-
lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
|
|
197
|
+
lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
|
|
198
|
+
lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`) ||
|
|
199
|
+
lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
|
|
193
200
|
|
|
194
201
|
if (!hasBunInstall) return null
|
|
195
202
|
if (!isFinalAlpine) return null
|
|
@@ -209,24 +216,26 @@ export function getBunCompileHint(fileContent) {
|
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
/**
|
|
212
|
-
* Перевіряє, що для `mirror.gcr.io/
|
|
219
|
+
* Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged` або
|
|
220
|
+
* `mirror.gcr.io/library/nginx`) у `FROM` вказано тег `alpine-slim` (docker.mdc).
|
|
213
221
|
*
|
|
214
222
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
215
223
|
* @returns {string | null} повідомлення помилки або null
|
|
216
224
|
*/
|
|
217
225
|
export function getNginxAlpineSlimTagHint(fileContent) {
|
|
226
|
+
const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX, NGINX_MIRROR_PREFIX]
|
|
218
227
|
for (const { line, image } of parseFromStages(fileContent)) {
|
|
219
228
|
const noDigest = (image.split('@')[0] || '').trim()
|
|
220
229
|
const d = noDigest.toLowerCase()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
const prefix = prefixes.find(p => d.startsWith(`${p}:`) || d === p)
|
|
231
|
+
if (prefix) {
|
|
232
|
+
if (d === prefix) {
|
|
233
|
+
return `рядок ${line}: \`FROM ${prefix}\` має явний тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз без тега (типово latest)`
|
|
234
|
+
}
|
|
235
|
+
const tag = noDigest.slice(prefix.length + 1)
|
|
236
|
+
if (tag.toLowerCase() !== 'alpine-slim') {
|
|
237
|
+
return `рядок ${line}: для nginx потрібен тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз: \`${tag}\``
|
|
238
|
+
}
|
|
230
239
|
}
|
|
231
240
|
}
|
|
232
241
|
return null
|
|
@@ -247,6 +256,8 @@ export function getNonRootRuntimeHint(fileContent) {
|
|
|
247
256
|
if (stages.length === 0) return null
|
|
248
257
|
|
|
249
258
|
const last = stages.at(-1)
|
|
259
|
+
const lastImage = (last?.from.image || '').split('@')[0] || ''
|
|
260
|
+
const lastLower = lastImage.toLowerCase()
|
|
250
261
|
const lastStageContent = last?.stageContent || ''
|
|
251
262
|
|
|
252
263
|
/** @type {string | null} */
|
|
@@ -259,6 +270,8 @@ export function getNonRootRuntimeHint(fileContent) {
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
if (!lastUserToken) {
|
|
273
|
+
// nginx-unprivileged вже запускається від uid=101 (nginx) без явного USER у Dockerfile
|
|
274
|
+
if (lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`)) return null
|
|
262
275
|
return 'у фінальному stage має бути `USER <non-root>` (наприклад `USER app`) — принцип non-root (docker.mdc: не превілейований образ)'
|
|
263
276
|
}
|
|
264
277
|
|
|
@@ -110,14 +110,15 @@ export function normalizeHubRepoPath(imageToken) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const HUB_REPOS_REQUIRING_MIRROR = /** @type {const} */ (
|
|
113
|
-
new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node'])
|
|
113
|
+
new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node', 'nginxinc/nginx-unprivileged'])
|
|
114
114
|
)
|
|
115
115
|
|
|
116
116
|
const EXPECTED_MIRROR = /** @type {const} */ ({
|
|
117
117
|
'oven/bun': 'mirror.gcr.io/oven/bun',
|
|
118
118
|
'library/alpine': 'mirror.gcr.io/library/alpine',
|
|
119
119
|
'library/nginx': 'mirror.gcr.io/library/nginx',
|
|
120
|
-
'library/node': 'mirror.gcr.io/library/node'
|
|
120
|
+
'library/node': 'mirror.gcr.io/library/node',
|
|
121
|
+
'nginxinc/nginx-unprivileged': 'mirror.gcr.io/nginxinc/nginx-unprivileged'
|
|
121
122
|
})
|
|
122
123
|
|
|
123
124
|
/**
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -47,6 +47,27 @@ bun run lint
|
|
|
47
47
|
- Якщо тестів **немає** або вони **не покривають** блок, який змінюєш — **спочатку** додай/розшир тести, переконайся, що вони стабільно проходять, **потім** роби рефакторинг, **потім** знову прогони тести й **`bun run lint`**, щоб підтвердити, що функціональність коректна й лінт чистий.
|
|
48
48
|
- Якщо після рефакторингу тести або лінт падають — **не** залишай «половинчастий» рефакторинг: відкотись або доведи зміни до зеленого стану.
|
|
49
49
|
|
|
50
|
+
## Навантаження на macOS (багато процесів `eslint` / лаги)
|
|
51
|
+
|
|
52
|
+
Часто це **не** «один ESLint розпаралелив себе всередині», а **кілька паралельних запусків** одного й того ж ланцюжка (**`bun run lint`**, **`bun run lint-js`**) у різних Bash-задачах агента (кілька shells). Кожен запуск = окремий процес **`eslint`** плюс **`oxlint`**, **`jscpd`** тощо — диск і CPU перегружаються.
|
|
53
|
+
|
|
54
|
+
**Чому взагалі кілька `eslint`:** оркестратор (Claude Code, Cursor тощо) може **розпаралелити** роботу: кілька **субагентів** / **паралельних Bash-задач** / **фонових shell**, і кожен **сам** виконує **`/n-lint`** або запускає **`eslint`**. Тоді навантаження **множиться**: не один прогон лінту, а **N прогонів** одночасно.
|
|
55
|
+
|
|
56
|
+
**Що робити агенту під час виконання цього скілу (обов’язково)**
|
|
57
|
+
|
|
58
|
+
1. **Один** запуск **`bun run lint`** (або всі кроки **`lint`**, як у **`package.json`**) — у **одному** foreground shell, **без** `run_in_background` / фонових копій тієї ж команди.
|
|
59
|
+
2. **Не** викликати **паралельні субагенти** (subagent, Task, «розбий на N паралельних завдань») лише заради лінту в одному репозиторії. Лінт не потребує шардінгу: один процес, послідовно.
|
|
60
|
+
3. Якщо ти **батьок-сесія** й уже делегував дочірнім задачам **інші** кроки — **не** доручай дочірнім і **`bun run lint`**, залиш **лише** собі один **послідовний** прогон **після** змін у файлах, або **один** виклик цілого скілу на сесію.
|
|
61
|
+
4. Якщо сесія/користувач уже запускає лінт — **не** дублювати; зачекати завершення або **не** стартувати лінт повторно в іншому shell.
|
|
62
|
+
|
|
63
|
+
**Що можна змінити у проєкті (локально або в `package.json`)**
|
|
64
|
+
|
|
65
|
+
- **ESLint** (CLI): за потреби явно **`--concurrency off`** (див. **`eslint --help`**).
|
|
66
|
+
- **oxlint**: **`--threads=1`**, якщо потрібно зменшити навантаження на CPU.
|
|
67
|
+
- **ESLint cache**: **`--cache`** / **`--cache-location .eslintcache`** — менше повторного читання з диска.
|
|
68
|
+
|
|
69
|
+
Канонічний рядок **`lint-js`** у репозиторіях з **`check js-lint`** фіксований; додаткові прапорці — з узгодженням канону або в споживацькому проєкті окремо.
|
|
70
|
+
|
|
50
71
|
## Примітка
|
|
51
72
|
|
|
52
73
|
Цей скіл **не** замінює **`npx @nitra/cursor check`**: **`lint`** перевіряє лінтери/формат у **`package.json`**, а **`check`** — програмні правила пакета **`@nitra/cursor`**. За потреби запускай обидва.
|