@nitra/cursor 1.8.209 → 1.8.212
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 +55 -0
- package/bin/auto-rules.md +2 -12
- package/bin/auto-skills.md +21 -0
- package/bin/n-cursor.js +18 -11
- package/mdc/js-bun-db.mdc +55 -1
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +12 -44
- package/scripts/auto-skills.mjs +78 -0
- package/scripts/check-js-bun-db.mjs +44 -4
- package/scripts/utils/bun-sql-scan.mjs +192 -0
- package/skills/taze/SKILL.md +106 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,61 @@
|
|
|
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.212] - 2026-05-08
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `npm/skills/taze/SKILL.md`: повний workflow замість шаблону-заглушки. Тепер
|
|
12
|
+
скіл бекапить `package.json`/`bun.lock`, виконує `bunx taze -w -r latest` +
|
|
13
|
+
`bun install`, виявляє major-стрибки порівнянням з бекапом, тягне breaking
|
|
14
|
+
changes з CHANGELOG модуля або git-діфу `node_modules` (з фолбеком на
|
|
15
|
+
встановлення старої версії в `/tmp`), шукає використання зачепленого API в
|
|
16
|
+
коді через `rg`, рефакторить несумісні місця (нетривіальні міграції — TODO),
|
|
17
|
+
прибирає тимчасові файли і віддає структурований звіт користувачу.
|
|
18
|
+
|
|
19
|
+
## [1.8.211] - 2026-05-08
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Окремий шлях автодетекту для скілів — `npm/bin/auto-skills.md` +
|
|
24
|
+
`npm/scripts/auto-skills.mjs` (`detectAutoSkills`). Скіли отримують свій
|
|
25
|
+
словник умов (`skill - [rules]`), залежний від уже виявлених правил, тож не
|
|
26
|
+
дублюють файлові ознаки з `auto-rules.md`.
|
|
27
|
+
- Нові авто-скіли: `publish-telegram` (завжди) і `taze` (за правилом `bun`).
|
|
28
|
+
- `npm/tests/auto-skills.test.mjs` — окремі тести `detectAutoSkills`
|
|
29
|
+
(завжди-додавані, залежності від rule-id, `disable-skills`, фільтр за
|
|
30
|
+
`availableSkills`).
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `npm/scripts/auto-rules.mjs`: `detectAutoRulesAndSkills` → `detectAutoRules`
|
|
35
|
+
(повертає лише `{ rules }`); прибрано `AUTO_SKILL_ORDER` і скіл-логіку.
|
|
36
|
+
`mergeConfigWithAutoDetected` лишається спільним і приймає вже виявлені
|
|
37
|
+
rules+skills, тож публічний контракт `.n-cursor.json` не змінився.
|
|
38
|
+
- `npm/bin/n-cursor.js` тепер послідовно викликає `detectAutoRules` і
|
|
39
|
+
`detectAutoSkills` (скіли отримують `detectedRules` як вхід).
|
|
40
|
+
- `npm/bin/auto-rules.md` залишає тільки правила; секція скілів винесена в
|
|
41
|
+
`auto-skills.md` з посиланням з `auto-rules.md`.
|
|
42
|
+
|
|
43
|
+
## [1.8.210] - 2026-05-08
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- `js-bun-db` v1.6: правило тепер забороняє локальні pg-format-сумісні шими у
|
|
48
|
+
файлах з Bun SQL.
|
|
49
|
+
- Розділ `## pg-format: повне видалення, без шимів` у `npm/mdc/js-bun-db.mdc`:
|
|
50
|
+
типові ідіоми `format(...)` → tagged template, заборонений drop-in `format()`
|
|
51
|
+
і `pg`-сумісна `query(text, params)`-обгортка над `sql.unsafe(...)`.
|
|
52
|
+
- Два нові AST-детектори у `npm/scripts/utils/bun-sql-scan.mjs`:
|
|
53
|
+
`findPgFormatShimDefinitionInText` (функції `format` / `pgFormat` /
|
|
54
|
+
`sqlFormat` / `pgFmt` з `%L`/`%I`/`%s` у тілі, плюс `quoteLiteral` /
|
|
55
|
+
`quoteIdent` / `escapeLiteral` / `escapeIdent` без додаткової перевірки)
|
|
56
|
+
та `findPgFormatLikeQueryWrapperInText` (`{ query(text, params) { ...
|
|
57
|
+
<obj>.unsafe(...) ... } }`). Скан запускається лише у файлах з
|
|
58
|
+
`import { sql|SQL } from 'bun'`.
|
|
59
|
+
- `npm/scripts/check-js-bun-db.mjs` рапортує `pgFormatShim` / `queryWrapper` —
|
|
60
|
+
окремі лічильники й `pass`-рядки, без зміни існуючих перевірок.
|
|
61
|
+
|
|
7
62
|
## [1.8.209] - 2026-05-08
|
|
8
63
|
|
|
9
64
|
### Removed
|
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
|
|
|
@@ -50,16 +50,6 @@ text - завжди
|
|
|
50
50
|
|
|
51
51
|
vue - якщо присутній хоч один vue файл
|
|
52
52
|
|
|
53
|
-
## Скіли, які автоматично додається до .n-cursor.json
|
|
54
|
-
|
|
55
|
-
abie-kustomize - якщо в кореневому package.json в секції "repository" присутній текст "<https://github.com/abinbevefes/**/>"
|
|
56
|
-
|
|
57
|
-
fix - завжди
|
|
58
|
-
|
|
59
|
-
lint - завжди
|
|
60
|
-
|
|
61
53
|
## Виключення
|
|
62
54
|
|
|
63
55
|
Якщо в .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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.6'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Підтримувані версії баз даних
|
|
@@ -18,6 +18,58 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
|
|
|
18
18
|
|
|
19
19
|
`pg-format` — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» не потрібен.
|
|
20
20
|
|
|
21
|
+
## `pg-format`: повне видалення, без шимів
|
|
22
|
+
|
|
23
|
+
Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
|
|
24
|
+
|
|
25
|
+
- функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
|
|
26
|
+
- допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
|
|
27
|
+
- обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
|
|
28
|
+
|
|
29
|
+
Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне `${value}` стає окремим параметром bind, без рядкового екранування.
|
|
30
|
+
|
|
31
|
+
### Типові ідіоми `pg-format` → Bun SQL
|
|
32
|
+
|
|
33
|
+
| Було (`pg-format`) | Стало (Bun SQL) |
|
|
34
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
35
|
+
| `format('... WHERE id = %L', id)` | `sql\`... WHERE id = ${id}\`` |
|
|
36
|
+
| `format('... IN (%L)', ids)` | `sql\`... IN ${sql(ids)}\`` (з guard на пустоту перед запитом) |
|
|
37
|
+
| `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
|
|
38
|
+
| `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
|
|
39
|
+
| `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
|
|
40
|
+
| `format('... %I ...', tableName)` (whitelist) | `sql.unsafe(\`... \${tableName} ...\`)` з маркером `// allow-unsafe: <причина>` і whitelist'ом |
|
|
41
|
+
|
|
42
|
+
Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
|
|
43
|
+
|
|
44
|
+
### Заборонений «drop-in» шим
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
// ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
|
|
48
|
+
export function format(fmt, ...args) {
|
|
49
|
+
let i = 0
|
|
50
|
+
return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
|
|
54
|
+
await sql.unsafe(format('... WHERE id = %L', userId))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
|
|
59
|
+
export const pgWrite = {
|
|
60
|
+
query(text, params) {
|
|
61
|
+
return sql.unsafe(text, params)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// ✅ напряму tagged template — параметризація через wire-protocol bind
|
|
68
|
+
await sql`... WHERE id = ${userId}`
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
|
|
72
|
+
|
|
21
73
|
## Підключення (singleton + env)
|
|
22
74
|
|
|
23
75
|
Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
|
|
@@ -197,6 +249,8 @@ function getUser(id) {
|
|
|
197
249
|
|
|
198
250
|
Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
|
|
199
251
|
|
|
252
|
+
Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (див. `## pg-format: повне видалення, без шимів`).
|
|
253
|
+
|
|
200
254
|
## Перевірка
|
|
201
255
|
|
|
202
256
|
`npx @nitra/cursor check js-bun-db`.
|
package/package.json
CHANGED
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
5
|
* залежності `mssql` / `pg` / `pg-format` / `mysql2` у `package.json`, імпорт `sql`/`SQL` з `bun`, кореневий
|
|
6
6
|
* `package.json`, `config.yaml` з рядком `metadata_directory: metadata` для hasura)
|
|
7
|
-
* та повертає ідентифікатори
|
|
7
|
+
* та повертає ідентифікатори правил, які потрібно автододати.
|
|
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'
|
|
@@ -47,9 +50,6 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
47
50
|
'vue'
|
|
48
51
|
])
|
|
49
52
|
|
|
50
|
-
/** Порядок автододавання skills відповідно до `auto-rules.md`. */
|
|
51
|
-
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
52
|
-
|
|
53
53
|
/**
|
|
54
54
|
* Карта міграції застарілих rule-id у `.n-cursor.json` на актуальні.
|
|
55
55
|
* Застосовується автоматично при читанні конфігу (як для `rules`, так і для `disable-rules`).
|
|
@@ -575,29 +575,23 @@ function resolveRuleDependencies(detectedRules, addRule) {
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
/**
|
|
578
|
-
* Визначає авто-правила
|
|
578
|
+
* Визначає авто-правила згідно з `auto-rules.md`.
|
|
579
579
|
* @param {object} params параметри аналізу
|
|
580
580
|
* @param {string} params.root абсолютний шлях до кореня репозиторію
|
|
581
581
|
* @param {string[]} params.availableRules перелік доступних правил з пакету
|
|
582
|
-
* @param {string[]} params.availableSkills перелік доступних skills з пакету
|
|
583
582
|
* @param {unknown} params.packageJsonParsed кореневий package.json (розпарсений) або null
|
|
584
583
|
* @param {string[]} [params.disableRules] список `disable-rules` з конфігу
|
|
585
|
-
* @
|
|
586
|
-
* @returns {Promise<{ rules: string[], skills: string[] }>} списки id у стабільному порядку
|
|
584
|
+
* @returns {Promise<{ rules: string[] }>} список id у стабільному порядку (за `AUTO_RULE_ORDER`)
|
|
587
585
|
*/
|
|
588
|
-
export async function
|
|
586
|
+
export async function detectAutoRules({
|
|
589
587
|
root,
|
|
590
588
|
availableRules,
|
|
591
|
-
availableSkills,
|
|
592
589
|
packageJsonParsed,
|
|
593
|
-
disableRules = DEFAULT_DISABLED_LIST
|
|
594
|
-
disableSkills = DEFAULT_DISABLED_LIST
|
|
590
|
+
disableRules = DEFAULT_DISABLED_LIST
|
|
595
591
|
}) {
|
|
596
592
|
const facts = await collectAutoRuleFacts(root)
|
|
597
593
|
const normalizedRules = new Set(availableRules.map(r => r.trim().toLowerCase()))
|
|
598
|
-
const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
|
|
599
594
|
const disableRulesSet = new Set(disableRules)
|
|
600
|
-
const disableSkillsSet = new Set(disableSkills)
|
|
601
595
|
|
|
602
596
|
const packageJsonExists = existsSync(join(root, 'package.json'))
|
|
603
597
|
const npmDirExists = existsSync(join(root, 'npm'))
|
|
@@ -616,8 +610,6 @@ export async function detectAutoRulesAndSkills({
|
|
|
616
610
|
|
|
617
611
|
/** @type {string[]} */
|
|
618
612
|
const detectedRules = []
|
|
619
|
-
/** @type {string[]} */
|
|
620
|
-
const detectedSkills = []
|
|
621
613
|
|
|
622
614
|
/**
|
|
623
615
|
* Додає правило до результату, якщо воно доступне і не в disable-списку.
|
|
@@ -631,18 +623,6 @@ export async function detectAutoRulesAndSkills({
|
|
|
631
623
|
detectedRules.push(ruleId)
|
|
632
624
|
}
|
|
633
625
|
|
|
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
626
|
const autoRuleChecks = [
|
|
647
627
|
{ enabled: isAbie, id: 'abie' },
|
|
648
628
|
{ enabled: packageJsonExists, id: 'bun' },
|
|
@@ -673,20 +653,8 @@ export async function detectAutoRulesAndSkills({
|
|
|
673
653
|
}
|
|
674
654
|
resolveRuleDependencies(detectedRules, addRule)
|
|
675
655
|
|
|
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
656
|
const rules = AUTO_RULE_ORDER.filter(ruleId => detectedRules.includes(ruleId))
|
|
688
|
-
|
|
689
|
-
return { rules, skills }
|
|
657
|
+
return { rules }
|
|
690
658
|
}
|
|
691
659
|
|
|
692
660
|
/**
|
|
@@ -0,0 +1,78 @@
|
|
|
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([
|
|
16
|
+
'abie-kustomize',
|
|
17
|
+
'fix',
|
|
18
|
+
'lint',
|
|
19
|
+
'publish-telegram',
|
|
20
|
+
'taze'
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Залежність скілів від правил (`auto-skills.md` синтаксис `skill - [rules]`).
|
|
25
|
+
* Ключ варто автододати, коли всі правила-залежності вже додані до конфігу автодетектом.
|
|
26
|
+
*/
|
|
27
|
+
export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
|
|
28
|
+
/** @type {Record<string, readonly string[]>} */ ({
|
|
29
|
+
'abie-kustomize': Object.freeze(['abie']),
|
|
30
|
+
taze: Object.freeze(['bun'])
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/** Скіли без залежностей — додаються завжди (рядок «завжди» в `auto-skills.md`). */
|
|
35
|
+
const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'publish-telegram'])
|
|
36
|
+
|
|
37
|
+
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Визначає авто-skills згідно з `auto-skills.md`.
|
|
41
|
+
* @param {object} params параметри
|
|
42
|
+
* @param {string[]} params.availableSkills перелік доступних skills із пакету (id без префікса n-)
|
|
43
|
+
* @param {string[]} params.detectedRules id правил, виявлених auto-rules (вхідні залежності)
|
|
44
|
+
* @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
|
|
45
|
+
* @returns {{ skills: string[] }} список id у стабільному порядку (за `AUTO_SKILL_ORDER`)
|
|
46
|
+
*/
|
|
47
|
+
export function detectAutoSkills({ availableSkills, detectedRules, disableSkills = DEFAULT_DISABLED_LIST }) {
|
|
48
|
+
const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
|
|
49
|
+
const disableSkillsSet = new Set(disableSkills)
|
|
50
|
+
const detectedRulesSet = new Set(detectedRules)
|
|
51
|
+
|
|
52
|
+
/** @type {string[]} */
|
|
53
|
+
const detected = []
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Додає skill до результату, якщо він доступний і не в disable-списку.
|
|
57
|
+
* @param {string} skillId id skill
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
function addSkill(skillId) {
|
|
61
|
+
if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detected.includes(skillId)) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
detected.push(skillId)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const skillId of ALWAYS_ON_SKILLS) {
|
|
68
|
+
addSkill(skillId)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const [skillId, deps] of Object.entries(AUTO_SKILL_RULE_DEPENDENCIES)) {
|
|
72
|
+
if (deps.every(d => detectedRulesSet.has(d))) {
|
|
73
|
+
addSkill(skillId)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { skills: AUTO_SKILL_ORDER.filter(id => detected.includes(id)) }
|
|
78
|
+
}
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
findBunSqlPerRequestConnectionInText,
|
|
31
31
|
findBunSqlPgLeftoverCallInText,
|
|
32
32
|
findBunSqlUnsafeUseWithoutAllowMarkerInText,
|
|
33
|
+
findPgFormatLikeQueryWrapperInText,
|
|
34
|
+
findPgFormatShimDefinitionInText,
|
|
33
35
|
findUnsafeBunSqlDynamicSqlListInText,
|
|
34
36
|
findUnsafeBunSqlInListMissingEmptyGuardInText,
|
|
35
37
|
isBunSqlScanSourceFile,
|
|
@@ -67,13 +69,21 @@ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
|
|
|
67
69
|
* @param {string[]} sourcePaths абсолютні шляхи джерел
|
|
68
70
|
* @param {string} repoRoot абсолютний шлях до кореня
|
|
69
71
|
* @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
|
|
70
|
-
* @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number }>}
|
|
72
|
+
* @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number }>}
|
|
71
73
|
* `hasBunSqlImport` — чи знайдено хоч один `import { sql|SQL } from 'bun'` у джерелах;
|
|
72
74
|
* решта — кількість порушень кожного типу.
|
|
73
75
|
*/
|
|
74
76
|
async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
75
77
|
const { fail } = reporter
|
|
76
|
-
const counts = {
|
|
78
|
+
const counts = {
|
|
79
|
+
perRequest: 0,
|
|
80
|
+
unsafeCall: 0,
|
|
81
|
+
dynamicList: 0,
|
|
82
|
+
inListGuard: 0,
|
|
83
|
+
pgLeftover: 0,
|
|
84
|
+
pgFormatShim: 0,
|
|
85
|
+
queryWrapper: 0
|
|
86
|
+
}
|
|
77
87
|
let hasBunSqlImport = false
|
|
78
88
|
|
|
79
89
|
for (const absPath of sourcePaths) {
|
|
@@ -93,7 +103,7 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
|
93
103
|
* @param {string} content вміст файлу
|
|
94
104
|
* @param {string} rel posix-шлях відносно `repoRoot`
|
|
95
105
|
* @param {(msg: string) => void} fail callback при помилці
|
|
96
|
-
* @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number }} counts акумулятори
|
|
106
|
+
* @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number }} counts акумулятори
|
|
97
107
|
* @returns {void}
|
|
98
108
|
*/
|
|
99
109
|
function scanFileForBunSqlPatterns(content, rel, fail, counts) {
|
|
@@ -134,6 +144,30 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
|
|
|
134
144
|
counts.inListGuard++
|
|
135
145
|
fail(messageForBunSqlInListGuard(rel, v))
|
|
136
146
|
}
|
|
147
|
+
for (const v of findPgFormatShimDefinitionInText(content, rel)) {
|
|
148
|
+
counts.pgFormatShim++
|
|
149
|
+
if (v.kind === 'format_function') {
|
|
150
|
+
fail(
|
|
151
|
+
`js-bun-db: ${rel}:${v.line} — функція ${JSON.stringify(v.name)} виглядає як pg-format-сумісний шим ` +
|
|
152
|
+
`(тіло містить %L / %I / %s). Видали шим і переведи всі call-site на tagged template ` +
|
|
153
|
+
`sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
|
|
154
|
+
)
|
|
155
|
+
} else {
|
|
156
|
+
fail(
|
|
157
|
+
`js-bun-db: ${rel}:${v.line} — ${JSON.stringify(v.name)} — це pg-format-специфічний escape-хелпер; ` +
|
|
158
|
+
`з Bun SQL він не потрібен (параметризація через tagged template), видали і перепиши call-site ` +
|
|
159
|
+
`(js-bun-db.mdc): ${v.snippet}`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const v of findPgFormatLikeQueryWrapperInText(content, rel)) {
|
|
164
|
+
counts.queryWrapper++
|
|
165
|
+
fail(
|
|
166
|
+
`js-bun-db: ${rel}:${v.line} — query(text, params)-обгортка над <obj>.unsafe(...) — це прихований ` +
|
|
167
|
+
`pg-сумісний шим. Видали обгортку (pgRead/pgWrite/db.query) і переведи всі call-site на tagged template ` +
|
|
168
|
+
`sql\`...\${value}...\` (js-bun-db.mdc): ${v.snippet}`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
137
171
|
}
|
|
138
172
|
|
|
139
173
|
/**
|
|
@@ -194,7 +228,7 @@ export async function check() {
|
|
|
194
228
|
return reporter.getExitCode()
|
|
195
229
|
}
|
|
196
230
|
|
|
197
|
-
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover } =
|
|
231
|
+
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover, pgFormatShim, queryWrapper } =
|
|
198
232
|
await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
|
|
199
233
|
|
|
200
234
|
if (!hasBunSqlImport) {
|
|
@@ -220,6 +254,12 @@ export async function check() {
|
|
|
220
254
|
if (inListGuard === 0) {
|
|
221
255
|
pass('js-bun-db: усі IN-списки винесені у змінні та мають перевірку на пустоту з throw')
|
|
222
256
|
}
|
|
257
|
+
if (pgFormatShim === 0) {
|
|
258
|
+
pass('js-bun-db: немає pg-format-сумісних шимів (format/quoteLiteral/quoteIdent/...) у файлах з Bun SQL')
|
|
259
|
+
}
|
|
260
|
+
if (queryWrapper === 0) {
|
|
261
|
+
pass('js-bun-db: немає query(text, params)-обгорток над unsafe(...) у файлах з Bun SQL')
|
|
262
|
+
}
|
|
223
263
|
|
|
224
264
|
return reporter.getExitCode()
|
|
225
265
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
offsetToLine,
|
|
26
26
|
parseProgramAndCommentsOrNull,
|
|
27
27
|
parseProgramOrNull,
|
|
28
|
+
templateQuasisText,
|
|
28
29
|
walkAstWithAncestors
|
|
29
30
|
} from './ast-scan-utils.mjs'
|
|
30
31
|
|
|
@@ -43,6 +44,18 @@ const ALLOW_PG_LEFTOVER_MARKER_RE = /\ballow-pg-leftover\s*:\s*\S+/u
|
|
|
43
44
|
// формально існують і там, тому опт-аут маркером лишається доречним.
|
|
44
45
|
const PG_LEFTOVER_METHOD_NAMES = new Set(['connect', 'end'])
|
|
45
46
|
|
|
47
|
+
// pg-format placeholders — `%L` (literal), `%I` (identifier), `%s` (raw string).
|
|
48
|
+
// Якщо у тілі функції з підозрілим іменем зустрічається такий літерал/regex —
|
|
49
|
+
// це pg-format-сумісний шим (drop-in замінник pg-format поверх Bun SQL).
|
|
50
|
+
const PG_FORMAT_PLACEHOLDER_RE = /%[LIs]/u
|
|
51
|
+
// Імена функцій-кандидатів на pg-format-шим. Спрацьовує лише у поєднанні
|
|
52
|
+
// з наявністю `%L` / `%I` / `%s` у тілі — щоб не плутати з невинним `format(date)`.
|
|
53
|
+
const PG_FORMAT_SHIM_FUNC_NAMES = new Set(['format', 'pgFormat', 'sqlFormat', 'pgFmt'])
|
|
54
|
+
// Імена quote/escape-хелперів — самі по собі сильний сигнал pg-format-шиму,
|
|
55
|
+
// без додаткової перевірки тіла. Це pg-format-специфічні API, нерідко публікуються
|
|
56
|
+
// як named export з модуля-обгортки.
|
|
57
|
+
const QUOTE_HELPER_NAMES = new Set(['quoteLiteral', 'quoteIdent', 'escapeLiteral', 'escapeIdent'])
|
|
58
|
+
|
|
46
59
|
/**
|
|
47
60
|
* @param {unknown} node AST node
|
|
48
61
|
* @param {string} name імʼя змінної
|
|
@@ -267,6 +280,185 @@ function asPgLeftoverCall(node) {
|
|
|
267
280
|
return { name: /** @type {'connect' | 'end'} */ (prop.name) }
|
|
268
281
|
}
|
|
269
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Чи це CallExpression `<obj>.unsafe(...)` (для пошуку в тілі query-шиму).
|
|
285
|
+
* Дублює `isUnsafeCall` з основного скану, але локально — щоб не залежати
|
|
286
|
+
* від порядку оголошень у файлі.
|
|
287
|
+
* @param {unknown} node AST node
|
|
288
|
+
* @returns {boolean} true для `<obj>.unsafe(...)`
|
|
289
|
+
*/
|
|
290
|
+
function isUnsafeCallNode(node) {
|
|
291
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
292
|
+
const callee = node.callee
|
|
293
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
|
|
294
|
+
const prop = callee.property
|
|
295
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'unsafe'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Чи містить піддерево вузла рядковий або regex-літерал з `%L` / `%I` / `%s`.
|
|
300
|
+
* Покриває:
|
|
301
|
+
* - `Literal` зі строковим `value`,
|
|
302
|
+
* - `StringLiteral` (oxc),
|
|
303
|
+
* - `TemplateLiteral` (через текст quasis),
|
|
304
|
+
* - `RegExpLiteral` / `Literal` з `regex.pattern`.
|
|
305
|
+
* @param {unknown} root корінь піддерева (зазвичай тіло функції)
|
|
306
|
+
* @returns {boolean} true, якщо знайдено pg-format-плейсхолдер
|
|
307
|
+
*/
|
|
308
|
+
function nodeContainsPgFormatPlaceholder(root) {
|
|
309
|
+
let found = false
|
|
310
|
+
walkAstWithAncestors(root, [], n => {
|
|
311
|
+
if (found) return
|
|
312
|
+
const t = n.type
|
|
313
|
+
if (t === 'Literal' || t === 'StringLiteral') {
|
|
314
|
+
if (typeof n.value === 'string' && PG_FORMAT_PLACEHOLDER_RE.test(n.value)) {
|
|
315
|
+
found = true
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
const regex = n.regex
|
|
319
|
+
if (regex && typeof regex.pattern === 'string' && PG_FORMAT_PLACEHOLDER_RE.test(regex.pattern)) {
|
|
320
|
+
found = true
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (t === 'RegExpLiteral' && typeof n.pattern === 'string' && PG_FORMAT_PLACEHOLDER_RE.test(n.pattern)) {
|
|
325
|
+
found = true
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if (t === 'TemplateLiteral') {
|
|
329
|
+
if (PG_FORMAT_PLACEHOLDER_RE.test(templateQuasisText(n))) {
|
|
330
|
+
found = true
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
return found
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Витягає (name, body) з вузла, що оголошує функцію верхнього рівня:
|
|
339
|
+
* - `function format(...) {...}`,
|
|
340
|
+
* - `const format = (...) => {...}` / `= function(...) {...}`.
|
|
341
|
+
* @param {Record<string, unknown>} node AST node
|
|
342
|
+
* @returns {{ name: string, body: unknown } | null} ім'я та тіло, або null
|
|
343
|
+
*/
|
|
344
|
+
function asNamedFunctionDecl(node) {
|
|
345
|
+
if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
346
|
+
return { name: node.id.name, body: node.body }
|
|
347
|
+
}
|
|
348
|
+
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
349
|
+
const init = node.init
|
|
350
|
+
if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
351
|
+
return { name: node.id.name, body: init.body }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Знаходить визначення pg-format-сумісних шимів у джерелі. Прапорує:
|
|
359
|
+
* - функції з іменами `format` / `pgFormat` / `sqlFormat` / `pgFmt`, у тілі яких
|
|
360
|
+
* зустрічається літерал/regex з `%L` / `%I` / `%s` — це drop-in pg-format;
|
|
361
|
+
* - функції з іменами `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent`
|
|
362
|
+
* незалежно від тіла — це pg-format-специфічні API, не потрібні з Bun SQL.
|
|
363
|
+
*
|
|
364
|
+
* Скан запускається лише в файлах, де є `import { sql|SQL } from 'bun'`, щоб
|
|
365
|
+
* не плутати, наприклад, форматер дат чи URL-escape з SQL-шимом.
|
|
366
|
+
* @param {string} content вихідний код
|
|
367
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
368
|
+
* @returns {{ line: number, snippet: string, kind: 'format_function' | 'quote_helper', name: string }[]} список порушень
|
|
369
|
+
*/
|
|
370
|
+
export function findPgFormatShimDefinitionInText(content, virtualPath = 'scan.ts') {
|
|
371
|
+
if (!textHasBunSqlImport(content)) return []
|
|
372
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
373
|
+
if (!program) return []
|
|
374
|
+
|
|
375
|
+
/** @type {{ line: number, snippet: string, kind: 'format_function' | 'quote_helper', name: string }[]} */
|
|
376
|
+
const out = []
|
|
377
|
+
walkAstWithAncestors(program, [], node => {
|
|
378
|
+
const decl = asNamedFunctionDecl(node)
|
|
379
|
+
if (!decl) return
|
|
380
|
+
/** @type {'format_function' | 'quote_helper' | null} */
|
|
381
|
+
let kind = null
|
|
382
|
+
if (QUOTE_HELPER_NAMES.has(decl.name)) {
|
|
383
|
+
kind = 'quote_helper'
|
|
384
|
+
} else if (PG_FORMAT_SHIM_FUNC_NAMES.has(decl.name) && nodeContainsPgFormatPlaceholder(decl.body)) {
|
|
385
|
+
kind = 'format_function'
|
|
386
|
+
}
|
|
387
|
+
if (!kind) return
|
|
388
|
+
out.push({
|
|
389
|
+
line: offsetToLine(content, node.start),
|
|
390
|
+
snippet: normalizeSnippet(content.slice(node.start, Math.min(node.end, node.start + 240))),
|
|
391
|
+
kind,
|
|
392
|
+
name: decl.name
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
return out
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Знаходить pg-сумісні query-обгортки виду
|
|
400
|
+
* `{ query(text, params) { return <sql>.unsafe(text, params) } }`
|
|
401
|
+
* у файлах, що імпортують Bun SQL. Така обгортка маскує `unsafe` під
|
|
402
|
+
* «безпечним» ім'ям і повертає injection-поверхню в код.
|
|
403
|
+
*
|
|
404
|
+
* Спрацьовує, коли всі умови виконані:
|
|
405
|
+
* - вузол — `Property` з `key.name === 'query'` всередині `ObjectExpression`;
|
|
406
|
+
* - значення — функція з 1–2 параметрами, перший — Identifier з типовим
|
|
407
|
+
* pg-іменем (`text` / `sql` / `query`);
|
|
408
|
+
* - у тілі функції є виклик `<obj>.unsafe(...)`.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} content вихідний код
|
|
411
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
412
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
413
|
+
*/
|
|
414
|
+
export function findPgFormatLikeQueryWrapperInText(content, virtualPath = 'scan.ts') {
|
|
415
|
+
if (!textHasBunSqlImport(content)) return []
|
|
416
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
417
|
+
if (!program) return []
|
|
418
|
+
|
|
419
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
420
|
+
const out = []
|
|
421
|
+
walkAstWithAncestors(program, [], node => {
|
|
422
|
+
if (node.type !== 'ObjectExpression') return
|
|
423
|
+
const properties = node.properties
|
|
424
|
+
if (!Array.isArray(properties)) return
|
|
425
|
+
for (const prop of properties) {
|
|
426
|
+
if (!prop || prop.type !== 'Property') continue
|
|
427
|
+
const key = prop.key
|
|
428
|
+
const keyName =
|
|
429
|
+
key && key.type === 'Identifier' ? key.name : key && key.type === 'Literal' ? key.value : null
|
|
430
|
+
if (keyName !== 'query') continue
|
|
431
|
+
const value = prop.value
|
|
432
|
+
if (!value || (value.type !== 'FunctionExpression' && value.type !== 'ArrowFunctionExpression')) continue
|
|
433
|
+
const params = value.params
|
|
434
|
+
const firstName = Array.isArray(params) && params[0]?.type === 'Identifier' ? params[0].name : null
|
|
435
|
+
const looksLikePgQuery =
|
|
436
|
+
Array.isArray(params) && params.length >= 1 && params.length <= 2 && /^(text|sql|query)$/u.test(firstName || '')
|
|
437
|
+
if (!looksLikePgQuery) continue
|
|
438
|
+
if (!nodeContainsUnsafeCall(value.body)) continue
|
|
439
|
+
out.push({
|
|
440
|
+
line: offsetToLine(content, prop.start),
|
|
441
|
+
snippet: normalizeSnippet(content.slice(prop.start, prop.end))
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
return out
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Чи є у піддереві виклик `<obj>.unsafe(...)`.
|
|
450
|
+
* @param {unknown} root корінь піддерева
|
|
451
|
+
* @returns {boolean} true, якщо знайдено
|
|
452
|
+
*/
|
|
453
|
+
function nodeContainsUnsafeCall(root) {
|
|
454
|
+
let found = false
|
|
455
|
+
walkAstWithAncestors(root, [], n => {
|
|
456
|
+
if (found) return
|
|
457
|
+
if (isUnsafeCallNode(n)) found = true
|
|
458
|
+
})
|
|
459
|
+
return found
|
|
460
|
+
}
|
|
461
|
+
|
|
270
462
|
/**
|
|
271
463
|
* Знаходить `new SQL(...)` всередині функцій (handler на кожен запит замість singleton).
|
|
272
464
|
* @param {string} content вихідний код
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
- **сумісно** — проект не використовує зачеплене API → нічого не робити.
|
|
70
|
+
- **несумісно** — використання знайдено → перейти до п. 6.
|
|
71
|
+
|
|
72
|
+
### 6. Рефакторинг несумісних місць
|
|
73
|
+
|
|
74
|
+
Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun run lint
|
|
78
|
+
bun run typecheck # якщо є
|
|
79
|
+
bun test # якщо є
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
|
|
83
|
+
|
|
84
|
+
### 7. Прибрати тимчасові файли
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
rm package.json.taze-bak bun.lock.taze-bak
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
(І решту бекапів воркспейсів, якщо створювались.)
|
|
91
|
+
|
|
92
|
+
### 8. Звіт користувачу
|
|
93
|
+
|
|
94
|
+
Коротко в одному повідомленні:
|
|
95
|
+
|
|
96
|
+
- **Оновлено (minor/patch):** кількість пакетів, без деталей.
|
|
97
|
+
- **Major-оновлення:** список `<name>: <old> → <new>` з посиланням на release notes.
|
|
98
|
+
- **Зрефакторено автоматично:** список файлів і коротко що саме змінено.
|
|
99
|
+
- **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
|
|
100
|
+
- **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
|
|
101
|
+
|
|
102
|
+
## Примітка
|
|
103
|
+
|
|
104
|
+
- Не запускати `bun run lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
|
|
105
|
+
- Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
|
|
106
|
+
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним.
|