@nitra/cursor 1.8.210 → 1.8.213
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 +66 -6
- package/bin/auto-rules.md +4 -12
- package/bin/auto-skills.md +21 -0
- package/bin/n-cursor.js +18 -11
- package/mdc/js-bun-db.mdc +1 -1
- package/mdc/js-bun-redis.mdc +21 -0
- package/package.json +1 -1
- package/policy/js_bun_redis/package_json/package_json.rego +37 -0
- package/scripts/auto-rules.mjs +25 -47
- package/scripts/auto-skills.mjs +72 -0
- package/scripts/check-hasura.mjs +1 -2
- package/scripts/check-js-bun-redis.mjs +98 -0
- package/scripts/check-k8s.mjs +2 -2
- package/scripts/lint-conftest.mjs +14 -6
- package/scripts/utils/bun-sql-scan.mjs +1 -2
- package/scripts/utils/conn-file-rules.mjs +6 -2
- package/scripts/utils/redis-imports.mjs +172 -0
- package/skills/taze/SKILL.md +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,66 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.213] - 2026-05-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Нове правило `js-bun-redis` (`npm/mdc/js-bun-redis.mdc`): заміна `ioredis` /
|
|
12
|
+
`node-redis` (включно з кореневим `redis` v4 і підпакетами `@redis/*`) на
|
|
13
|
+
Bun native Redis (`import { redis } from 'bun'`,
|
|
14
|
+
<https://bun.com/docs/runtime/redis>).
|
|
15
|
+
- AST-сканер `npm/scripts/utils/redis-imports.mjs` (`oxc-parser`) ловить
|
|
16
|
+
`import` / `require` / динамічний `import()` пакетів `ioredis`, `node-redis`,
|
|
17
|
+
`redis`, підшляхів `ioredis/...` / `redis/...` і `@redis/*`. Не зачіпає
|
|
18
|
+
сторонні `redis-*` (наприклад, `redis-mock`).
|
|
19
|
+
- `npm/scripts/check-js-bun-redis.mjs` запускає AST-скан по JS/TS-джерелах і
|
|
20
|
+
доступний як `npx @nitra/cursor check js-bun-redis`.
|
|
21
|
+
- Rego-полісі `npm/policy/js_bun_redis/package_json/` — заборона
|
|
22
|
+
`ioredis` / `node-redis` / `redis` / `@redis/*` у `dependencies` будь-якого
|
|
23
|
+
`package.json` у дереві; зареєстрована таргетом у
|
|
24
|
+
`npm/scripts/lint-conftest.mjs` (`bun run lint-conftest`).
|
|
25
|
+
- Авто-увімкнення правила в `.n-cursor.json`: `npm/scripts/auto-rules.mjs`
|
|
26
|
+
додає `js-bun-redis`, якщо в `dependencies` хоч одного `package.json` є
|
|
27
|
+
`ioredis` або `node-redis` (умова — у `npm/bin/auto-rules.md`).
|
|
28
|
+
- Тести: `npm/tests/redis-imports.test.mjs` (AST-сканер) і нові кейси у
|
|
29
|
+
`npm/tests/auto-rules.test.mjs` (детект `ioredis` / `node-redis`).
|
|
30
|
+
|
|
31
|
+
## [1.8.212] - 2026-05-08
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `npm/skills/taze/SKILL.md`: повний workflow замість шаблону-заглушки. Тепер
|
|
36
|
+
скіл бекапить `package.json`/`bun.lock`, виконує `bunx taze -w -r latest` +
|
|
37
|
+
`bun install`, виявляє major-стрибки порівнянням з бекапом, тягне breaking
|
|
38
|
+
changes з CHANGELOG модуля або git-діфу `node_modules` (з фолбеком на
|
|
39
|
+
встановлення старої версії в `/tmp`), шукає використання зачепленого API в
|
|
40
|
+
коді через `rg`, рефакторить несумісні місця (нетривіальні міграції — TODO),
|
|
41
|
+
прибирає тимчасові файли і віддає структурований звіт користувачу.
|
|
42
|
+
|
|
43
|
+
## [1.8.211] - 2026-05-08
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Окремий шлях автодетекту для скілів — `npm/bin/auto-skills.md` +
|
|
48
|
+
`npm/scripts/auto-skills.mjs` (`detectAutoSkills`). Скіли отримують свій
|
|
49
|
+
словник умов (`skill - [rules]`), залежний від уже виявлених правил, тож не
|
|
50
|
+
дублюють файлові ознаки з `auto-rules.md`.
|
|
51
|
+
- Нові авто-скіли: `publish-telegram` (завжди) і `taze` (за правилом `bun`).
|
|
52
|
+
- `npm/tests/auto-skills.test.mjs` — окремі тести `detectAutoSkills`
|
|
53
|
+
(завжди-додавані, залежності від rule-id, `disable-skills`, фільтр за
|
|
54
|
+
`availableSkills`).
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- `npm/scripts/auto-rules.mjs`: `detectAutoRulesAndSkills` → `detectAutoRules`
|
|
59
|
+
(повертає лише `{ rules }`); прибрано `AUTO_SKILL_ORDER` і скіл-логіку.
|
|
60
|
+
`mergeConfigWithAutoDetected` лишається спільним і приймає вже виявлені
|
|
61
|
+
rules+skills, тож публічний контракт `.n-cursor.json` не змінився.
|
|
62
|
+
- `npm/bin/n-cursor.js` тепер послідовно викликає `detectAutoRules` і
|
|
63
|
+
`detectAutoSkills` (скіли отримують `detectedRules` як вхід).
|
|
64
|
+
- `npm/bin/auto-rules.md` залишає тільки правила; секція скілів винесена в
|
|
65
|
+
`auto-skills.md` з посиланням з `auto-rules.md`.
|
|
66
|
+
|
|
7
67
|
## [1.8.210] - 2026-05-08
|
|
8
68
|
|
|
9
69
|
### Added
|
|
@@ -18,7 +78,7 @@
|
|
|
18
78
|
`sqlFormat` / `pgFmt` з `%L`/`%I`/`%s` у тілі, плюс `quoteLiteral` /
|
|
19
79
|
`quoteIdent` / `escapeLiteral` / `escapeIdent` без додаткової перевірки)
|
|
20
80
|
та `findPgFormatLikeQueryWrapperInText` (`{ query(text, params) { ...
|
|
21
|
-
|
|
81
|
+
<obj>.unsafe(...) ... } }`). Скан запускається лише у файлах з
|
|
22
82
|
`import { sql|SQL } from 'bun'`.
|
|
23
83
|
- `npm/scripts/check-js-bun-db.mjs` рапортує `pgFormatShim` / `queryWrapper` —
|
|
24
84
|
окремі лічильники й `pass`-рядки, без зміни існуючих перевірок.
|
|
@@ -69,10 +129,10 @@
|
|
|
69
129
|
(prettier-залежність, `@nitra/eslint-config ≥ 3.9.2`),
|
|
70
130
|
`checkPackageJsonTypeModule` для root, `checkEnginesNode/Bun` для root,
|
|
71
131
|
канонічний `lint-js`-скрипт, валідація `lint-js.yml` (`verifyLintJsWorkflowStructure`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
132
|
+
- fallback). Лишилися — `.oxlintrc.json` canonical-snapshot, VSCode-розширення,
|
|
133
|
+
workspace-ітерація для `type: "module"` і engines, дубль JS-кроків у `lint.yml`,
|
|
134
|
+
`.jscpd.json`. Прибрано непотрібні імпорти `parseWorkflowYaml`,
|
|
135
|
+
`verifyLintJsWorkflowStructure` і `OXLINT_FIX_RE`.
|
|
76
136
|
- `npm/scripts/check-js-run.mjs` — без перевірок `bunyan` / `@nitra/bunyan` у
|
|
77
137
|
залежностях, canonical `jsconfig.json` через `deepEqualJson`,
|
|
78
138
|
`OTEL_RESOURCE_ATTRIBUTES` у `configmap.yaml`. Лишилися AST-скан коду
|
|
@@ -184,7 +244,7 @@
|
|
|
184
244
|
- `check-k8s.mjs` (автоконверт `image-replace` patches → `images:`): тепер працює і для `patches[i].patch` із **кількома** ops, а не лише з одинокою image-replace op. Сканує всі ops у патчі, конвертує **кожну** `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:`; якщо всі ops патча конвертовано — `patches[i]` видаляється повністю; інакше inline `patch:` переписується через `parseDocument` без конвертованих ops зі збереженням block-literal scalar (`|-`) і вихідного порядку решти ops. Реалізовано через нові функції `tryParseJson6902Array` (≥ 1 op, замість `tryParseSingleJson6902Array`) і `rewriteInlinePatchWithoutOps`; `imageReplaceDeploymentPatchInfo` повертає `{ deployName, totalOps, ops: [{ containerIndex, newImage, opIndex }] }` (раніше — одиничний `{ deployName, containerIndex, newImage }` лише за `length === 1`); `applyConversionsToDoc` групує конвертації по індексу патча й вирізає ops або сам патч за потреби. Сортування решти ops після видалення лишається поза цією зміною — за нього відповідає окрема перевірка `kustomizationInlinePatchOpsSortedViolation`.
|
|
185
245
|
- `mdc/k8s.mdc` (v1.26 → v1.27): уточнено крок 1 авто-перевірки в розділі «Зміна image — через `images:`, не через `patches[]`» — тепер описує і випадок, коли в `patches[i].patch` лишаються не-image ops (їх зберігає, у вихідному порядку, без коментарів).
|
|
186
246
|
- `check-js-lint.mjs` + `mdc/js-lint.mdc` (v1.16 → v1.17): мінімум `@nitra/eslint-config` піднято з `^3.8.0` до `^3.9.2`. Обґрунтування: з 3.9.2 у `getConfig` вбудовано ignore для `**/adr/**`, тож ADR-документи не валідуються ESLint, і консьюмерам не треба додавати цей glob у `eslint.config.js` локально. `nitraEslintConfigMeetsMinVersion` тепер повертає `false` для діапазонів `^3.8.x`–`^3.9.1`; `workspace:*` лишається ok без змін. Pass/fail-повідомлення `checkPackageJsonLintDeps` оновлено під новий мінімум; `for...in`-бан з 3.8.0 згадується як накопичена відмінність. Тести `nitraEslintConfigMeetsMinVersion` розширено: `^3.9.2`/`^3.9.10`/`^3.10.0`/`^4.0.0` — ok; `^3.9.1`/`^3.8.0`/`^3.6.12`/`^3.4.3` — ні.
|
|
187
|
-
- `bin/n-cursor.js` (`reexecIfPackageVersionChanged` + `spawnSync`-виклик): `process.env.NITRA_CURSOR_REEXEC` і `...process.env` замінено на `env.NITRA_CURSOR_REEXEC` і `...env` з `node:process` (`import { cwd, env } from 'node:process'`). Підстава: правило `js-run.mdc` забороняє прямий `process.env.*` у Node-коді; `NITRA_CURSOR_REEXEC` — опційна змінна (виставляється лише при re-exec), тож імпорт `env` з `node:process` (а не з `@nitra/check-env`) — канонічна форма для опційних. Поведінка не змінена; раніше `npm/scripts/check-js-run.mjs` помилявся на `bin/n-cursor.js:1136` (правило `process-env`), тепер
|
|
247
|
+
- `bin/n-cursor.js` (`reexecIfPackageVersionChanged` + `spawnSync`-виклик): `process.env.NITRA_CURSOR_REEXEC` і `...process.env` замінено на `env.NITRA_CURSOR_REEXEC` і `...env` з `node:process` (`import { cwd, env } from 'node:process'`). Підстава: правило `js-run.mdc` забороняє прямий `process.env.*` у Node-коді; `NITRA_CURSOR_REEXEC` — опційна змінна (виставляється лише при re-exec), тож імпорт `env` з `node:process` (а не з `@nitra/check-env`) — канонічна форма для опційних. Поведінка не змінена; раніше `npm/scripts/check-js-run.mjs` помилявся на `bin/n-cursor.js:1136` (правило `process-env`), тепер integration-test `check-* на реальному репозиторії` проходить.
|
|
188
248
|
|
|
189
249
|
### Added
|
|
190
250
|
|
package/bin/auto-rules.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Авто вмикання правил
|
|
1
|
+
# Авто вмикання правил
|
|
2
2
|
|
|
3
|
-
В цьому файлі описані умови, по яким повинні
|
|
3
|
+
В цьому файлі описані умови, по яким повинні правила автододаватись в конфіг. Умови автододавання скілів — у `auto-skills.md` (виносить логіку зі словника правил у пару «правило → скіл»).
|
|
4
4
|
|
|
5
5
|
## Правила, які автоматично додається до .n-cursor.json
|
|
6
6
|
|
|
@@ -34,6 +34,8 @@ js-mssql - якщо в хоч одному package.json в секції dependen
|
|
|
34
34
|
|
|
35
35
|
js-bun-db - якщо в хоч одному package.json в секції dependencies присутній пакет pg, pg-format або mysql2 або є імпорт sql/SQL з Bun (приклад: import { sql } from "bun")
|
|
36
36
|
|
|
37
|
+
js-bun-redis - якщо в хоч одному package.json в секції dependencies присутній пакет ioredis або node-redis
|
|
38
|
+
|
|
37
39
|
k8s - якщо присутня хоч одна директорія k8s
|
|
38
40
|
|
|
39
41
|
nginx-default-tpl - якщо присутній хоч один файл з переліку - default.conf.template, default.conf, nginx.conf
|
|
@@ -50,16 +52,6 @@ text - завжди
|
|
|
50
52
|
|
|
51
53
|
vue - якщо присутній хоч один vue файл
|
|
52
54
|
|
|
53
|
-
## Скіли, які автоматично додається до .n-cursor.json
|
|
54
|
-
|
|
55
|
-
abie-kustomize - якщо в кореневому package.json в секції "repository" присутній текст "<https://github.com/abinbevefes/**/>"
|
|
56
|
-
|
|
57
|
-
fix - завжди
|
|
58
|
-
|
|
59
|
-
lint - завжди
|
|
60
|
-
|
|
61
55
|
## Виключення
|
|
62
56
|
|
|
63
57
|
Якщо в .n-cursor.json задано в секції disable-rules правило, то воно автоматично додаватись не повинно.
|
|
64
|
-
|
|
65
|
-
Якщо в .n-cursor.json задано в секції disable-skills скіл, то він автоматично додаватись не повинен.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Авто вмикання скілів
|
|
2
|
+
|
|
3
|
+
В цьому файлі описані умови, по яким повинні скіли автододаватись в конфіг.
|
|
4
|
+
|
|
5
|
+
## Скіли, які автоматично додається до .n-cursor.json
|
|
6
|
+
|
|
7
|
+
Синтаксис `skill - [rules]` означає: скіл `skill` варто автододати лише якщо всі правила у списку `[rules]` уже додані до конфігу автодетектом (з `auto-rules.md`). Так залежність не дублює вихідну умову правила.
|
|
8
|
+
|
|
9
|
+
abie-kustomize - [abie]
|
|
10
|
+
|
|
11
|
+
fix - завжди
|
|
12
|
+
|
|
13
|
+
lint - завжди
|
|
14
|
+
|
|
15
|
+
publish-telegram - завжди
|
|
16
|
+
|
|
17
|
+
taze - [bun]
|
|
18
|
+
|
|
19
|
+
## Виключення
|
|
20
|
+
|
|
21
|
+
Якщо в .n-cursor.json задано в секції disable-skills скіл, то він автоматично додаватись не повинен.
|
package/bin/n-cursor.js
CHANGED
|
@@ -58,12 +58,13 @@ import { fileURLToPath } from 'node:url'
|
|
|
58
58
|
|
|
59
59
|
import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
|
|
60
60
|
import {
|
|
61
|
-
|
|
61
|
+
detectAutoRules,
|
|
62
62
|
detectLegacyRuleIds,
|
|
63
63
|
mergeConfigWithAutoDetected,
|
|
64
64
|
normalizeIdList,
|
|
65
65
|
RULE_MIGRATIONS
|
|
66
66
|
} from '../scripts/auto-rules.mjs'
|
|
67
|
+
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
67
68
|
import { runStopHookCli } from '../scripts/claude-stop-hook.mjs'
|
|
68
69
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
69
70
|
import { runLintGaCli } from '../scripts/lint-ga.mjs'
|
|
@@ -250,19 +251,22 @@ async function readConfig(paths = {}) {
|
|
|
250
251
|
const rootPkg = await readRootPackageJsonSafe()
|
|
251
252
|
const disableRules = normalizeIdList(parsedConfig['disable-rules'])
|
|
252
253
|
const disableSkills = normalizeIdList(parsedConfig['disable-skills'])
|
|
253
|
-
const
|
|
254
|
+
const autoDetectedRules = await detectAutoRules({
|
|
254
255
|
root: cwd(),
|
|
255
256
|
availableRules,
|
|
256
|
-
availableSkills,
|
|
257
257
|
packageJsonParsed: rootPkg,
|
|
258
|
-
disableRules
|
|
258
|
+
disableRules
|
|
259
|
+
})
|
|
260
|
+
const autoDetectedSkills = detectAutoSkills({
|
|
261
|
+
availableSkills,
|
|
262
|
+
detectedRules: autoDetectedRules.rules,
|
|
259
263
|
disableSkills
|
|
260
264
|
})
|
|
261
265
|
|
|
262
266
|
const merged = mergeConfigWithAutoDetected({
|
|
263
267
|
config: parsedConfig,
|
|
264
|
-
detectedRules:
|
|
265
|
-
detectedSkills:
|
|
268
|
+
detectedRules: autoDetectedRules.rules,
|
|
269
|
+
detectedSkills: autoDetectedSkills.skills
|
|
266
270
|
})
|
|
267
271
|
|
|
268
272
|
const rest = Object.fromEntries(Object.entries(parsedConfig).filter(([k]) => k !== '$schema'))
|
|
@@ -283,16 +287,19 @@ async function readConfig(paths = {}) {
|
|
|
283
287
|
|
|
284
288
|
if (!existsSync(configPath)) {
|
|
285
289
|
const rootPkg = await readRootPackageJsonSafe()
|
|
286
|
-
const
|
|
290
|
+
const autoDetectedRules = await detectAutoRules({
|
|
287
291
|
root: cwd(),
|
|
288
292
|
availableRules,
|
|
289
|
-
availableSkills,
|
|
290
293
|
packageJsonParsed: rootPkg
|
|
291
294
|
})
|
|
295
|
+
const autoDetectedSkills = detectAutoSkills({
|
|
296
|
+
availableSkills,
|
|
297
|
+
detectedRules: autoDetectedRules.rules
|
|
298
|
+
})
|
|
292
299
|
const defaultConfig = sortConfigIdArrays({
|
|
293
300
|
$schema: CONFIG_SCHEMA_URL,
|
|
294
|
-
rules:
|
|
295
|
-
skills:
|
|
301
|
+
rules: autoDetectedRules.rules,
|
|
302
|
+
skills: autoDetectedSkills.skills
|
|
296
303
|
})
|
|
297
304
|
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
|
|
298
305
|
console.log(
|
|
@@ -1127,7 +1134,7 @@ async function readBundledVersionAt(packageRoot) {
|
|
|
1127
1134
|
* Якщо `upgradeNitraCursorToLatestAndBunInstall` встановив у `node_modules/@nitra/cursor` версію,
|
|
1128
1135
|
* відмінну від тієї, з якої стартував поточний процес (наприклад, з npx-кешу), запускає бінар нової
|
|
1129
1136
|
* версії через `spawnSync` і завершує поточний процес із успадкованим exit-кодом. Re-exec потрібен,
|
|
1130
|
-
* бо ES-модулі вже завантажені у V8 (RULE_MIGRATIONS,
|
|
1137
|
+
* бо ES-модулі вже завантажені у V8 (RULE_MIGRATIONS, detectAutoRules тощо) і нова логіка
|
|
1131
1138
|
* без повної заміни процесу не підхопиться. Захист від нескінченного циклу — env `NITRA_CURSOR_REEXEC=1`.
|
|
1132
1139
|
* @param {string} effectivePackageRoot шлях, повернутий `upgradeNitraCursorToLatestAndBunInstall`
|
|
1133
1140
|
* @returns {Promise<void>} повертається лише якщо re-exec не потрібен (інакше викликає `process.exit`)
|
package/mdc/js-bun-db.mdc
CHANGED
|
@@ -26,7 +26,7 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
|
|
|
26
26
|
- допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
|
|
27
27
|
- обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
|
|
28
28
|
|
|
29
|
-
Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``.
|
|
29
|
+
Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне`${value}` стає окремим параметром bind, без рядкового екранування.
|
|
30
30
|
|
|
31
31
|
### Типові ідіоми `pg-format` → Bun SQL
|
|
32
32
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Використання Redis/Valkey з Bun
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Підтримувані версії redis
|
|
8
|
+
|
|
9
|
+
Redis 7.2+
|
|
10
|
+
|
|
11
|
+
## Заміна на Bun native Redis
|
|
12
|
+
|
|
13
|
+
Якщо в проєкті використовуються бібліотеки `ioredis`, `node-redis`, їх потрібно замінити на Bun native Redis: <https://bun.com/docs/runtime/redis>.
|
|
14
|
+
|
|
15
|
+
- Видалити з `dependencies`: `ioredis`, `node-redis`.
|
|
16
|
+
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
17
|
+
- Замінити на `import { redis } from 'bun'`
|
|
18
|
+
|
|
19
|
+
## Перевірка
|
|
20
|
+
|
|
21
|
+
`npx @nitra/cursor check js-bun-redis`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Перевірка `dependencies` для правила `js-bun-redis.mdc` — паралель до
|
|
2
|
+
# `npm/policy/js_bun_db/package_json/package_json.rego`.
|
|
3
|
+
#
|
|
4
|
+
# Запуск (локально, для будь-якого `package.json` у дереві):
|
|
5
|
+
# conftest test path/to/package.json -p npm/policy/js_bun_redis \
|
|
6
|
+
# --namespace js_bun_redis.package_json
|
|
7
|
+
#
|
|
8
|
+
# Перевіряє: у `dependencies` не повинно бути `ioredis`, `node-redis`,
|
|
9
|
+
# `redis` або жодного з підпакетів `@redis/*` — заміна на Bun native Redis
|
|
10
|
+
# (https://bun.com/docs/runtime/redis).
|
|
11
|
+
#
|
|
12
|
+
# AST-скан коду (`import` / `require` / dynamic `import()` тих самих пакетів)
|
|
13
|
+
# лишається у `npm/scripts/check-js-bun-redis.mjs` (потребує `oxc-parser`).
|
|
14
|
+
#
|
|
15
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
16
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
17
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
18
|
+
package js_bun_redis.package_json
|
|
19
|
+
|
|
20
|
+
import rego.v1
|
|
21
|
+
|
|
22
|
+
forbidden_dependencies := {
|
|
23
|
+
"ioredis",
|
|
24
|
+
"node-redis",
|
|
25
|
+
"redis",
|
|
26
|
+
"@redis/client",
|
|
27
|
+
"@redis/json",
|
|
28
|
+
"@redis/search",
|
|
29
|
+
"@redis/time-series",
|
|
30
|
+
"@redis/bloom",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deny contains msg if {
|
|
34
|
+
some pkg_name in forbidden_dependencies
|
|
35
|
+
pkg_name in object.keys(object.get(input, "dependencies", {}))
|
|
36
|
+
msg := sprintf("dependencies містить заборонений %q — заміни на Bun native Redis (js-bun-redis.mdc)", [pkg_name])
|
|
37
|
+
}
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Автовизначення правил
|
|
2
|
+
* Автовизначення правил для `.n-cursor.json` за умовами з `npm/bin/auto-rules.md`.
|
|
3
3
|
*
|
|
4
4
|
* Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source,
|
|
5
|
-
* залежності `mssql` / `pg` / `pg-format` / `mysql2`
|
|
6
|
-
* `package.json`, `config.yaml` з рядком
|
|
7
|
-
* та повертає ідентифікатори
|
|
5
|
+
* залежності `mssql` / `pg` / `pg-format` / `mysql2` / `ioredis` / `node-redis` у `package.json`,
|
|
6
|
+
* імпорт `sql`/`SQL` з `bun`, кореневий `package.json`, `config.yaml` з рядком
|
|
7
|
+
* `metadata_directory: metadata` для hasura) та повертає ідентифікатори правил, які потрібно автододати.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Враховує винятки `disable-rules`: елементи зі списку не додаються автоматично.
|
|
10
|
+
*
|
|
11
|
+
* Автодетект скілів — у `./auto-skills.mjs` (умови — у `npm/bin/auto-skills.md`).
|
|
12
|
+
* `mergeConfigWithAutoDetected` нижче приймає вже виявлені rules і skills і вливає
|
|
13
|
+
* їх у конфіг із поправкою на legacy-id (`migrateRuleIds`).
|
|
11
14
|
*/
|
|
12
15
|
import { existsSync } from 'node:fs'
|
|
13
16
|
import { readdir, readFile } from 'node:fs/promises'
|
|
@@ -36,6 +39,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
36
39
|
'js-lint',
|
|
37
40
|
'js-mssql',
|
|
38
41
|
'js-bun-db',
|
|
42
|
+
'js-bun-redis',
|
|
39
43
|
'js-run',
|
|
40
44
|
'k8s',
|
|
41
45
|
'nginx-default-tpl',
|
|
@@ -47,9 +51,6 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
47
51
|
'vue'
|
|
48
52
|
])
|
|
49
53
|
|
|
50
|
-
/** Порядок автододавання skills відповідно до `auto-rules.md`. */
|
|
51
|
-
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
52
|
-
|
|
53
54
|
/**
|
|
54
55
|
* Карта міграції застарілих rule-id у `.n-cursor.json` на актуальні.
|
|
55
56
|
* Застосовується автоматично при читанні конфігу (як для `rules`, так і для `disable-rules`).
|
|
@@ -575,29 +576,23 @@ function resolveRuleDependencies(detectedRules, addRule) {
|
|
|
575
576
|
}
|
|
576
577
|
|
|
577
578
|
/**
|
|
578
|
-
* Визначає авто-правила
|
|
579
|
+
* Визначає авто-правила згідно з `auto-rules.md`.
|
|
579
580
|
* @param {object} params параметри аналізу
|
|
580
581
|
* @param {string} params.root абсолютний шлях до кореня репозиторію
|
|
581
582
|
* @param {string[]} params.availableRules перелік доступних правил з пакету
|
|
582
|
-
* @param {string[]} params.availableSkills перелік доступних skills з пакету
|
|
583
583
|
* @param {unknown} params.packageJsonParsed кореневий package.json (розпарсений) або null
|
|
584
584
|
* @param {string[]} [params.disableRules] список `disable-rules` з конфігу
|
|
585
|
-
* @
|
|
586
|
-
* @returns {Promise<{ rules: string[], skills: string[] }>} списки id у стабільному порядку
|
|
585
|
+
* @returns {Promise<{ rules: string[] }>} список id у стабільному порядку (за `AUTO_RULE_ORDER`)
|
|
587
586
|
*/
|
|
588
|
-
export async function
|
|
587
|
+
export async function detectAutoRules({
|
|
589
588
|
root,
|
|
590
589
|
availableRules,
|
|
591
|
-
availableSkills,
|
|
592
590
|
packageJsonParsed,
|
|
593
|
-
disableRules = DEFAULT_DISABLED_LIST
|
|
594
|
-
disableSkills = DEFAULT_DISABLED_LIST
|
|
591
|
+
disableRules = DEFAULT_DISABLED_LIST
|
|
595
592
|
}) {
|
|
596
593
|
const facts = await collectAutoRuleFacts(root)
|
|
597
594
|
const normalizedRules = new Set(availableRules.map(r => r.trim().toLowerCase()))
|
|
598
|
-
const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
|
|
599
595
|
const disableRulesSet = new Set(disableRules)
|
|
600
|
-
const disableSkillsSet = new Set(disableSkills)
|
|
601
596
|
|
|
602
597
|
const packageJsonExists = existsSync(join(root, 'package.json'))
|
|
603
598
|
const npmDirExists = existsSync(join(root, 'npm'))
|
|
@@ -608,16 +603,22 @@ export async function detectAutoRulesAndSkills({
|
|
|
608
603
|
: null
|
|
609
604
|
)
|
|
610
605
|
const isAbie = typeof repositoryUrl === 'string' && repositoryUrl.toLowerCase().includes(ABIE_REPOSITORY_URL_MARKER)
|
|
611
|
-
const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, [
|
|
606
|
+
const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, [
|
|
607
|
+
'mssql',
|
|
608
|
+
'pg',
|
|
609
|
+
'pg-format',
|
|
610
|
+
'mysql2',
|
|
611
|
+
'ioredis',
|
|
612
|
+
'node-redis'
|
|
613
|
+
])
|
|
612
614
|
const hasMssqlDependency = depHits.has('mssql')
|
|
613
615
|
const hasJsBunDbSignal =
|
|
614
616
|
depHits.has('pg') || depHits.has('pg-format') || depHits.has('mysql2') || facts.hasBunSqlImport
|
|
617
|
+
const hasJsBunRedisSignal = depHits.has('ioredis') || depHits.has('node-redis')
|
|
615
618
|
const hasNestedNodePackage = await hasNestedPackageJsonWithoutViteDevDependency(root)
|
|
616
619
|
|
|
617
620
|
/** @type {string[]} */
|
|
618
621
|
const detectedRules = []
|
|
619
|
-
/** @type {string[]} */
|
|
620
|
-
const detectedSkills = []
|
|
621
622
|
|
|
622
623
|
/**
|
|
623
624
|
* Додає правило до результату, якщо воно доступне і не в disable-списку.
|
|
@@ -631,18 +632,6 @@ export async function detectAutoRulesAndSkills({
|
|
|
631
632
|
detectedRules.push(ruleId)
|
|
632
633
|
}
|
|
633
634
|
|
|
634
|
-
/**
|
|
635
|
-
* Додає skill до результату, якщо він доступний і не в disable-списку.
|
|
636
|
-
* @param {string} skillId id skill
|
|
637
|
-
* @returns {void}
|
|
638
|
-
*/
|
|
639
|
-
function addSkill(skillId) {
|
|
640
|
-
if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detectedSkills.includes(skillId)) {
|
|
641
|
-
return
|
|
642
|
-
}
|
|
643
|
-
detectedSkills.push(skillId)
|
|
644
|
-
}
|
|
645
|
-
|
|
646
635
|
const autoRuleChecks = [
|
|
647
636
|
{ enabled: isAbie, id: 'abie' },
|
|
648
637
|
{ enabled: packageJsonExists, id: 'bun' },
|
|
@@ -654,6 +643,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
654
643
|
{ enabled: facts.hasJsLikeSource, id: 'js-lint' },
|
|
655
644
|
{ enabled: hasMssqlDependency, id: 'js-mssql' },
|
|
656
645
|
{ enabled: hasJsBunDbSignal, id: 'js-bun-db' },
|
|
646
|
+
{ enabled: hasJsBunRedisSignal, id: 'js-bun-redis' },
|
|
657
647
|
{ enabled: hasNestedNodePackage, id: 'js-run' },
|
|
658
648
|
{ enabled: facts.hasK8sDir, id: 'k8s' },
|
|
659
649
|
{ enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
|
|
@@ -673,20 +663,8 @@ export async function detectAutoRulesAndSkills({
|
|
|
673
663
|
}
|
|
674
664
|
resolveRuleDependencies(detectedRules, addRule)
|
|
675
665
|
|
|
676
|
-
const autoSkillChecks = [
|
|
677
|
-
{ enabled: isAbie, id: 'abie-kustomize' },
|
|
678
|
-
{ enabled: true, id: 'fix' },
|
|
679
|
-
{ enabled: true, id: 'lint' }
|
|
680
|
-
]
|
|
681
|
-
for (const item of autoSkillChecks) {
|
|
682
|
-
if (item.enabled) {
|
|
683
|
-
addSkill(item.id)
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
666
|
const rules = AUTO_RULE_ORDER.filter(ruleId => detectedRules.includes(ruleId))
|
|
688
|
-
|
|
689
|
-
return { rules, skills }
|
|
667
|
+
return { rules }
|
|
690
668
|
}
|
|
691
669
|
|
|
692
670
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Автовизначення skills для `.n-cursor.json` за умовами з `npm/bin/auto-skills.md`.
|
|
3
|
+
*
|
|
4
|
+
* Скіли автододаються залежно від уже виявлених правил (auto-rules) — щоб не дублювати
|
|
5
|
+
* умови, які вже формалізовані для відповідного правила. Наприклад:
|
|
6
|
+
*
|
|
7
|
+
* - `abie-kustomize - [abie]` — додається разом з правилом `abie`
|
|
8
|
+
* - `taze - [bun]` — додається разом з правилом `bun`
|
|
9
|
+
*
|
|
10
|
+
* Скіли без секції `[rules]` у `auto-skills.md` (`fix`, `lint`, `publish-telegram`)
|
|
11
|
+
* додаються завжди, якщо доступні в пакеті й не у `disable-skills`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Порядок автододавання skills відповідно до `auto-skills.md`. */
|
|
15
|
+
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint', 'publish-telegram', 'taze'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Залежність скілів від правил (`auto-skills.md` синтаксис `skill - [rules]`).
|
|
19
|
+
* Ключ варто автододати, коли всі правила-залежності вже додані до конфігу автодетектом.
|
|
20
|
+
*/
|
|
21
|
+
export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
|
|
22
|
+
/** @type {Record<string, readonly string[]>} */ ({
|
|
23
|
+
'abie-kustomize': Object.freeze(['abie']),
|
|
24
|
+
taze: Object.freeze(['bun'])
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
/** Скіли без залежностей — додаються завжди (рядок «завжди» в `auto-skills.md`). */
|
|
29
|
+
const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'publish-telegram'])
|
|
30
|
+
|
|
31
|
+
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Визначає авто-skills згідно з `auto-skills.md`.
|
|
35
|
+
* @param {object} params параметри
|
|
36
|
+
* @param {string[]} params.availableSkills перелік доступних skills із пакету (id без префікса n-)
|
|
37
|
+
* @param {string[]} params.detectedRules id правил, виявлених auto-rules (вхідні залежності)
|
|
38
|
+
* @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
|
|
39
|
+
* @returns {{ skills: string[] }} список id у стабільному порядку (за `AUTO_SKILL_ORDER`)
|
|
40
|
+
*/
|
|
41
|
+
export function detectAutoSkills({ availableSkills, detectedRules, disableSkills = DEFAULT_DISABLED_LIST }) {
|
|
42
|
+
const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
|
|
43
|
+
const disableSkillsSet = new Set(disableSkills)
|
|
44
|
+
const detectedRulesSet = new Set(detectedRules)
|
|
45
|
+
|
|
46
|
+
/** @type {string[]} */
|
|
47
|
+
const detected = []
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Додає skill до результату, якщо він доступний і не в disable-списку.
|
|
51
|
+
* @param {string} skillId id skill
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
function addSkill(skillId) {
|
|
55
|
+
if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detected.includes(skillId)) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
detected.push(skillId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const skillId of ALWAYS_ON_SKILLS) {
|
|
62
|
+
addSkill(skillId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [skillId, deps] of Object.entries(AUTO_SKILL_RULE_DEPENDENCIES)) {
|
|
66
|
+
if (deps.every(d => detectedRulesSet.has(d))) {
|
|
67
|
+
addSkill(skillId)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { skills: AUTO_SKILL_ORDER.filter(id => detected.includes(id)) }
|
|
72
|
+
}
|
package/scripts/check-hasura.mjs
CHANGED
|
@@ -148,9 +148,8 @@ async function checkEnvFile(relPath, expected, reporter) {
|
|
|
148
148
|
const value = m[1].trim()
|
|
149
149
|
const parsed = parseInternalHasuraEndpoint(value)
|
|
150
150
|
if (!parsed.ok) {
|
|
151
|
-
|
|
152
151
|
const example =
|
|
153
|
-
|
|
152
|
+
'https://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
|
|
154
153
|
fail(
|
|
155
154
|
`${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
|
|
156
155
|
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє правило `js-bun-redis.mdc`.
|
|
3
|
+
*
|
|
4
|
+
* Заборонено в JS/TS-джерелах будь-який `import` / `require` / динамічний `import()` пакетів
|
|
5
|
+
* `ioredis`, `node-redis`, `redis` (та підпакетів `@redis/*`, підшляхів `ioredis/...` /
|
|
6
|
+
* `redis/...`). Замість них треба використовувати Bun native Redis:
|
|
7
|
+
* `import { redis } from 'bun'` (<https://bun.com/docs/runtime/redis>).
|
|
8
|
+
*
|
|
9
|
+
* Перевірку `dependencies` (заборона `ioredis` / `node-redis` / `redis` / `@redis/*` у будь-якому
|
|
10
|
+
* `package.json`) винесено в Rego-полісі `npm/policy/js_bun_redis/package_json/`; її запускає
|
|
11
|
+
* `bun run lint-conftest`. Тут лишився AST-скан коду через `oxc-parser`.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { readFile } from 'node:fs/promises'
|
|
15
|
+
import { join, relative } from 'node:path'
|
|
16
|
+
|
|
17
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
18
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
19
|
+
import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from './utils/redis-imports.mjs'
|
|
20
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану заборонених redis-імпортів.
|
|
24
|
+
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
25
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
26
|
+
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
27
|
+
*/
|
|
28
|
+
async function findAllSourcePathsForRedisScan(repoRoot, ignorePaths) {
|
|
29
|
+
/** @type {string[]} */
|
|
30
|
+
const paths = []
|
|
31
|
+
await walkDir(
|
|
32
|
+
repoRoot,
|
|
33
|
+
absPath => {
|
|
34
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
35
|
+
if (isRedisScanSourceFile(rel) && !shouldSkipFileForRedisScan(rel)) {
|
|
36
|
+
paths.push(absPath)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
ignorePaths
|
|
40
|
+
)
|
|
41
|
+
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
42
|
+
return paths
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Сканує JS/TS-джерела на заборонені імпорти/require пакетів `ioredis` / `node-redis` / `redis`.
|
|
47
|
+
* @param {string[]} sourcePaths абсолютні шляхи джерел
|
|
48
|
+
* @param {string} repoRoot абсолютний шлях до кореня
|
|
49
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
50
|
+
* @returns {Promise<number>} кількість знайдених порушень
|
|
51
|
+
*/
|
|
52
|
+
async function scanSourcesForRedisImports(sourcePaths, repoRoot, fail) {
|
|
53
|
+
let violations = 0
|
|
54
|
+
for (const absPath of sourcePaths) {
|
|
55
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
56
|
+
const content = await readFile(absPath, 'utf8')
|
|
57
|
+
for (const v of findRedisImportsInText(content, rel)) {
|
|
58
|
+
violations++
|
|
59
|
+
fail(
|
|
60
|
+
`js-bun-redis: ${rel}:${v.line} — заміни '${v.module}' на Bun native Redis ` +
|
|
61
|
+
`(import { redis } from 'bun', https://bun.com/docs/runtime/redis): ${v.snippet}`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return violations
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Перевіряє відповідність проєкту правилу `js-bun-redis.mdc`.
|
|
70
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
71
|
+
*/
|
|
72
|
+
export async function check() {
|
|
73
|
+
const reporter = createCheckReporter()
|
|
74
|
+
const { pass, fail } = reporter
|
|
75
|
+
|
|
76
|
+
const repoRoot = process.cwd()
|
|
77
|
+
if (!existsSync(join(repoRoot, 'package.json'))) {
|
|
78
|
+
pass('js-bun-redis: package.json у корені відсутній — перевірку пропущено')
|
|
79
|
+
return reporter.getExitCode()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ignorePaths = await loadCursorIgnorePaths(repoRoot)
|
|
83
|
+
const sourcePaths = await findAllSourcePathsForRedisScan(repoRoot, ignorePaths)
|
|
84
|
+
if (sourcePaths.length === 0) {
|
|
85
|
+
pass('js-bun-redis: немає JS/TS файлів для скану імпортів ioredis / node-redis / redis')
|
|
86
|
+
return reporter.getExitCode()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const violations = await scanSourcesForRedisImports(sourcePaths, repoRoot, fail)
|
|
90
|
+
if (violations === 0) {
|
|
91
|
+
pass(
|
|
92
|
+
"js-bun-redis: немає імпортів 'ioredis' / 'node-redis' / 'redis' / '@redis/*' у джерелах " +
|
|
93
|
+
'(використовується Bun native Redis або redis взагалі не задіяно)'
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return reporter.getExitCode()
|
|
98
|
+
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -479,7 +479,7 @@ async function validateKustomizationResourcesSortedAlphabetically(root, yamlFile
|
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
/**
|
|
482
|
-
* Лексичне порівняння двох
|
|
482
|
+
* Лексичне порівняння двох tuple рядків через `localeCompare('en', { sensitivity: 'base' })`.
|
|
483
483
|
* Менший за довжиною список доповнюється порожніми рядками.
|
|
484
484
|
* @param {string[]} a перший tuple
|
|
485
485
|
* @param {string[]} b другий tuple
|
|
@@ -594,7 +594,7 @@ function jsonPointerPathsAreDisjoint(paths) {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
/**
|
|
597
|
-
* Парсить рядок JSON6902
|
|
597
|
+
* Парсить рядок JSON6902-патча в плоский масив операцій `{ op, path }` (без значень).
|
|
598
598
|
* Повертає `null`, якщо це не YAML-масив об'єктів з полями `op`/`path` як рядки.
|
|
599
599
|
* @param {string} raw тіло inline `patch:` (literal block scalar)
|
|
600
600
|
* @returns {{ op: string, path: string }[] | null} нормалізований список ops або `null` за невідповідного формату
|
|
@@ -183,6 +183,12 @@ const TARGETS = [
|
|
|
183
183
|
rule: 'js-bun-db',
|
|
184
184
|
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
namespace: 'js_bun_redis.package_json',
|
|
188
|
+
policyDir: 'js_bun_redis',
|
|
189
|
+
rule: 'js-bun-redis',
|
|
190
|
+
walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
|
|
191
|
+
},
|
|
186
192
|
{
|
|
187
193
|
namespace: 'js_run.package_json',
|
|
188
194
|
policyDir: 'js_run',
|
|
@@ -257,7 +263,10 @@ function collectFiles(root, match) {
|
|
|
257
263
|
continue
|
|
258
264
|
}
|
|
259
265
|
if (!e.isFile()) continue
|
|
260
|
-
const rel = abs
|
|
266
|
+
const rel = abs
|
|
267
|
+
.slice(root.length + 1)
|
|
268
|
+
.split(sep)
|
|
269
|
+
.join('/')
|
|
261
270
|
if (match(rel)) out.push(rel)
|
|
262
271
|
}
|
|
263
272
|
}
|
|
@@ -297,11 +306,10 @@ function runConftestForTarget(conftestBin, target, files) {
|
|
|
297
306
|
return 0
|
|
298
307
|
}
|
|
299
308
|
console.log(`\n▶ conftest (${target.namespace} — ${files.length} файл(ів))`)
|
|
300
|
-
const r = spawnSync(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
)
|
|
309
|
+
const r = spawnSync(conftestBin, ['test', ...files, '-p', policyAbs, '--namespace', target.namespace, '--no-color'], {
|
|
310
|
+
stdio: 'inherit',
|
|
311
|
+
env: process.env
|
|
312
|
+
})
|
|
305
313
|
if (r.error) {
|
|
306
314
|
console.error(`❌ Не вдалося запустити conftest: ${r.error.message}`)
|
|
307
315
|
return 1
|
|
@@ -425,8 +425,7 @@ export function findPgFormatLikeQueryWrapperInText(content, virtualPath = 'scan.
|
|
|
425
425
|
for (const prop of properties) {
|
|
426
426
|
if (!prop || prop.type !== 'Property') continue
|
|
427
427
|
const key = prop.key
|
|
428
|
-
const keyName =
|
|
429
|
-
key && key.type === 'Identifier' ? key.name : key && key.type === 'Literal' ? key.value : null
|
|
428
|
+
const keyName = key && key.type === 'Identifier' ? key.name : key && key.type === 'Literal' ? key.value : null
|
|
430
429
|
if (keyName !== 'query') continue
|
|
431
430
|
const value = prop.value
|
|
432
431
|
if (!value || (value.type !== 'FunctionExpression' && value.type !== 'ArrowFunctionExpression')) continue
|
|
@@ -100,7 +100,7 @@ function collectNamedExportNames(program) {
|
|
|
100
100
|
(decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&
|
|
101
101
|
decl.id &&
|
|
102
102
|
typeof decl.id === 'object' &&
|
|
103
|
-
typeof /** @type {Record<string, unknown>} */ (decl.id).name === 'string'
|
|
103
|
+
typeof (/** @type {Record<string, unknown>} */ (decl.id).name) === 'string'
|
|
104
104
|
) {
|
|
105
105
|
out.push(/** @type {string} */ (/** @type {Record<string, unknown>} */ (decl.id).name))
|
|
106
106
|
}
|
|
@@ -128,7 +128,11 @@ function hasDefaultExport(program) {
|
|
|
128
128
|
const body = /** @type {Record<string, unknown>} */ (program).body
|
|
129
129
|
if (!Array.isArray(body)) return false
|
|
130
130
|
for (const node of body) {
|
|
131
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
node &&
|
|
133
|
+
typeof node === 'object' &&
|
|
134
|
+
/** @type {Record<string, unknown>} */ (node).type === 'ExportDefaultDeclaration'
|
|
135
|
+
) {
|
|
132
136
|
return true
|
|
133
137
|
}
|
|
134
138
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить імпорти з `ioredis` та `node-redis` (та підшляхів `redis/...`) у джерелах —
|
|
3
|
+
* їх треба замінити на Bun native Redis (`import { redis } from 'bun'`) згідно з
|
|
4
|
+
* `js-bun-redis.mdc` (<https://bun.com/docs/runtime/redis>).
|
|
5
|
+
*
|
|
6
|
+
* Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
|
|
7
|
+
* Додатково по AST програми ловимо `require('ioredis')` і динамічний `import('ioredis')`,
|
|
8
|
+
* щоб правило працювало і у CommonJS, і при динамічному `import` у межах одного файлу.
|
|
9
|
+
*
|
|
10
|
+
* `node-redis` публікується під рядом імен:
|
|
11
|
+
* - кореневий пакет `redis` (саме так його імпортують у v4+);
|
|
12
|
+
* - історичний `node-redis` (рідше);
|
|
13
|
+
* - підпакети, які тягнуться разом: `@redis/client`, `@redis/json`, `@redis/search`,
|
|
14
|
+
* `@redis/time-series`, `@redis/bloom` — їх теж треба прибирати разом із основним
|
|
15
|
+
* клієнтом, щоб не лишилось «половини» інтеграції після переходу на Bun.
|
|
16
|
+
*
|
|
17
|
+
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
|
|
18
|
+
* результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
|
|
19
|
+
*/
|
|
20
|
+
import { parseSync } from 'oxc-parser'
|
|
21
|
+
|
|
22
|
+
import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.mjs'
|
|
23
|
+
|
|
24
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
25
|
+
const FORBIDDEN_MODULE_NAMES = new Set([
|
|
26
|
+
'ioredis',
|
|
27
|
+
'node-redis',
|
|
28
|
+
'redis',
|
|
29
|
+
'@redis/client',
|
|
30
|
+
'@redis/json',
|
|
31
|
+
'@redis/search',
|
|
32
|
+
'@redis/time-series',
|
|
33
|
+
'@redis/bloom'
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Чи є рядок-специфікатор імпорту забороненим (`ioredis`, `node-redis`, `redis`, `redis/...`,
|
|
38
|
+
* `ioredis/...`, `@redis/<sub>`).
|
|
39
|
+
*
|
|
40
|
+
* Використовуємо префікс-збіг для `ioredis/` та `redis/` — щоб ловити підшляхи
|
|
41
|
+
* (`ioredis/built/utils`, `redis/dist/...`), але не зачепити сторонні пакети
|
|
42
|
+
* на кшталт `redis-mock`, які треба валідувати окремо.
|
|
43
|
+
* @param {string} mod рядкове значення з `import '...'` / `require('...')`
|
|
44
|
+
* @returns {boolean} true, якщо такий specifier треба викинути на користь Bun native Redis
|
|
45
|
+
*/
|
|
46
|
+
function isForbiddenRedisModule(mod) {
|
|
47
|
+
if (FORBIDDEN_MODULE_NAMES.has(mod)) return true
|
|
48
|
+
return mod.startsWith('ioredis/') || mod.startsWith('redis/') || mod.startsWith('@redis/')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
|
|
53
|
+
* @param {Record<string, unknown> | null | undefined} node вузол AST
|
|
54
|
+
* @returns {string | null} ім'я модуля з аргументу, інакше `null`
|
|
55
|
+
*/
|
|
56
|
+
function requireCallModule(node) {
|
|
57
|
+
if (!node || node.type !== 'CallExpression') return null
|
|
58
|
+
const callee = node.callee
|
|
59
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return null
|
|
60
|
+
const arg = node.arguments?.[0]
|
|
61
|
+
if (!arg || arg.type !== 'Literal' || typeof arg.value !== 'string') return null
|
|
62
|
+
return arg.value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
|
|
67
|
+
* @param {Record<string, unknown> | null | undefined} node вузол AST
|
|
68
|
+
* @returns {string | null} ім'я модуля, інакше `null`
|
|
69
|
+
*/
|
|
70
|
+
function dynamicImportModule(node) {
|
|
71
|
+
if (!node || node.type !== 'ImportExpression') return null
|
|
72
|
+
const src = node.source
|
|
73
|
+
if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return null
|
|
74
|
+
return src.value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Простий рекурсивний обхід AST: заходимо в усі обʼєкти/масиви, щоб знайти require/import-вузли.
|
|
79
|
+
* @param {unknown} node корінь або під-вузол AST
|
|
80
|
+
* @param {(n: unknown) => void} visit виклик для кожного обʼєкта-вузла
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function walkAst(node, visit) {
|
|
84
|
+
if (!node || typeof node !== 'object') return
|
|
85
|
+
if (Array.isArray(node)) {
|
|
86
|
+
for (const item of node) walkAst(item, visit)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (typeof node.type === 'string') {
|
|
90
|
+
visit(node)
|
|
91
|
+
}
|
|
92
|
+
for (const key of Object.keys(node)) {
|
|
93
|
+
if (key !== 'parent') {
|
|
94
|
+
const v = node[key]
|
|
95
|
+
if (v && typeof v === 'object') walkAst(v, visit)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Знаходить заборонені імпорти/require з `ioredis` / `node-redis` у тексті.
|
|
102
|
+
* @param {string} content вихідний код
|
|
103
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
104
|
+
* @returns {{ line: number, snippet: string, module: string }[]} список порушень
|
|
105
|
+
*/
|
|
106
|
+
export function findRedisImportsInText(content, virtualPath = 'scan.ts') {
|
|
107
|
+
const pathForLang = virtualPath || 'scan.ts'
|
|
108
|
+
const lang = langFromPath(pathForLang)
|
|
109
|
+
let result
|
|
110
|
+
try {
|
|
111
|
+
result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
|
|
112
|
+
} catch {
|
|
113
|
+
return []
|
|
114
|
+
}
|
|
115
|
+
if (result.errors?.length) {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @type {{ line: number, snippet: string, module: string }[]} */
|
|
120
|
+
const out = []
|
|
121
|
+
|
|
122
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
123
|
+
const mod = imp.moduleRequest?.value
|
|
124
|
+
if (typeof mod === 'string' && isForbiddenRedisModule(mod)) {
|
|
125
|
+
out.push({
|
|
126
|
+
line: offsetToLine(content, imp.start),
|
|
127
|
+
snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
|
|
128
|
+
module: mod
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
walkAst(result.program, node => {
|
|
134
|
+
const reqMod = requireCallModule(node)
|
|
135
|
+
if (reqMod && isForbiddenRedisModule(reqMod)) {
|
|
136
|
+
out.push({
|
|
137
|
+
line: offsetToLine(content, node.start),
|
|
138
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
139
|
+
module: reqMod
|
|
140
|
+
})
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const dynMod = dynamicImportModule(node)
|
|
144
|
+
if (dynMod && isForbiddenRedisModule(dynMod)) {
|
|
145
|
+
out.push({
|
|
146
|
+
line: offsetToLine(content, node.start),
|
|
147
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
148
|
+
module: dynMod
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сімʼя).
|
|
158
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
159
|
+
* @returns {boolean} `true`, якщо розширення підходить для пошуку імпорту
|
|
160
|
+
*/
|
|
161
|
+
export function isRedisScanSourceFile(relativePath) {
|
|
162
|
+
return SOURCE_FILE_RE.test(relativePath)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Чи слід пропустити файл під час обходу пакета (декларації типів — лише типи, не виконувані).
|
|
167
|
+
* @param {string} relativePosix шлях з posix-слешами
|
|
168
|
+
* @returns {boolean} `true`, якщо файл не сканувати
|
|
169
|
+
*/
|
|
170
|
+
export function shouldSkipFileForRedisScan(relativePosix) {
|
|
171
|
+
return relativePosix.endsWith('.d.ts')
|
|
172
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-taze
|
|
3
|
+
description: >-
|
|
4
|
+
Оновлення версій модулів проекту з аналізом major-змін і автоматичним
|
|
5
|
+
рефакторингом несумісного коду
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# n-taze — Оновлення версій проекту
|
|
9
|
+
|
|
10
|
+
## Мета
|
|
11
|
+
|
|
12
|
+
Оновити всі модулі проекту до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
|
|
13
|
+
|
|
14
|
+
## Передумови
|
|
15
|
+
|
|
16
|
+
- Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
|
|
17
|
+
- Встановлений `bun` і доступний `bunx`.
|
|
18
|
+
- Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### 1. Зафіксувати стартовий стан
|
|
23
|
+
|
|
24
|
+
Перед оновленням зберегти список поточних версій, щоб потім обчислити, які модулі стрибнули через major:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cp package.json package.json.taze-bak
|
|
28
|
+
cp bun.lock bun.lock.taze-bak
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
(У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видалити в кінці.)
|
|
32
|
+
|
|
33
|
+
### 2. Запустити оновлення
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bunx taze -w -r latest
|
|
37
|
+
bun install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `-w` — записати нові версії у `package.json`.
|
|
41
|
+
- `-r` — рекурсивно по всіх воркспейсах.
|
|
42
|
+
- `latest` — піднімати навіть major.
|
|
43
|
+
|
|
44
|
+
### 3. Виявити major-оновлення
|
|
45
|
+
|
|
46
|
+
Порівняти `package.json.taze-bak` з оновленим `package.json` (а також `bun.lock.taze-bak` ↔ `bun.lock` для транзитивних). Для кожної залежності, у якої змінилась перша значуща цифра semver (`1.x.x → 2.x.x`, `0.4.x → 0.5.x`, `0.0.x → 0.1.x`) — додати в список «потребує перевірки».
|
|
47
|
+
|
|
48
|
+
Minor/patch — пропускати, їх вважаємо сумісними.
|
|
49
|
+
|
|
50
|
+
### 4. Зібрати breaking changes по кожному major-оновленню
|
|
51
|
+
|
|
52
|
+
Для кожного модуля зі списку зібрати фактичні відмінності одним з джерел (у порядку пріоритету):
|
|
53
|
+
|
|
54
|
+
1. **CHANGELOG / Releases репозиторію модуля** — найшвидше і найточніше. Адресу репозиторію взяти з `package.json` модуля у `node_modules/<name>/package.json` (поле `repository`). Дістати релізи між старою і новою версією.
|
|
55
|
+
2. **Git-різниця в `node_modules/<name>`** — якщо CHANGELOG відсутній або неінформативний, порівняти попередню версію (з кешу bun: `~/.bun/install/cache/<name>@<old-version>/`) з новою (`node_modules/<name>/`) через `diff -r` по `dist/` / `*.d.ts` / публічних entry-points з `exports`.
|
|
56
|
+
3. **Якщо немає кешованої старої версії** — встановити її окремо в тимчасову теку (`mkdir -p /tmp/taze-old && cd /tmp/taze-old && bun add <name>@<old-version>`) і порівняти.
|
|
57
|
+
|
|
58
|
+
Цікавлять: видалені/перейменовані експорти, змінені сигнатури функцій, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
|
|
59
|
+
|
|
60
|
+
### 5. Перевірити сумісність з кодом проекту
|
|
61
|
+
|
|
62
|
+
Для кожного breaking change знайти його використання в коді проекту:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Класифікувати:
|
|
69
|
+
|
|
70
|
+
- **сумісно** — проект не використовує зачеплене API → нічого не робити.
|
|
71
|
+
- **несумісно** — використання знайдено → перейти до п. 6.
|
|
72
|
+
|
|
73
|
+
### 6. Рефакторинг несумісних місць
|
|
74
|
+
|
|
75
|
+
Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun run lint
|
|
79
|
+
bun run typecheck # якщо є
|
|
80
|
+
bun test # якщо є
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
|
|
84
|
+
|
|
85
|
+
### 7. Прибрати тимчасові файли
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
rm package.json.taze-bak bun.lock.taze-bak
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
(І решту бекапів воркспейсів, якщо створювались.)
|
|
92
|
+
|
|
93
|
+
### 8. Звіт користувачу
|
|
94
|
+
|
|
95
|
+
Коротко в одному повідомленні:
|
|
96
|
+
|
|
97
|
+
- **Оновлено (minor/patch):** кількість пакетів, без деталей.
|
|
98
|
+
- **Major-оновлення:** список `<name>: <old> → <new>` з посиланням на release notes.
|
|
99
|
+
- **Зрефакторено автоматично:** список файлів і коротко що саме змінено.
|
|
100
|
+
- **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
|
|
101
|
+
- **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
|
|
102
|
+
|
|
103
|
+
## Примітка
|
|
104
|
+
|
|
105
|
+
- Не запускати `bun run lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
|
|
106
|
+
- Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
|
|
107
|
+
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним.
|