@7n/rules 1.25.1 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/bin/n-rules.js +16 -1
- package/package.json +1 -1
- package/scripts/lib/docs/plugin-api.md +1 -2
- package/scripts/lib/docs/resolve-plugins.md +1 -1
- package/scripts/lib/plugin-api.mjs +33 -4
- package/scripts/lib/resolve-plugins.mjs +2 -1
- package/skills/taze/SKILL.md +31 -79
- package/skills/taze/js/docs/orchestrate.md +9 -12
- package/skills/taze/js/orchestrate.mjs +25 -153
- package/skills/taze/js/diff.mjs +0 -140
- package/skills/taze/js/docs/diff.md +0 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.26.0] - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Фаза 5a spec lang-plugins-extraction: ядро — двигун без мовної специфіки. npm/bun-гілка taze виїхала в плагін `@7n/rules-lang-js` (EcosystemProvider; бекап воркспейсів, `bunx taze -w -r latest` + `bun install`, collectTazeDiff, SKILL-фрагмент); semver-примітиви (parseVersion/isBreaking) тепер визначені в `@7n/rules/plugin-api` (було — реекспорт із diff.mjs). Оркестратор — чистий цикл провайдерів (без вбудованих гілок), звіт — рівноправні секції екосистем; CLI `n-rules taze diff` резолвиться через handler lang-js. Автодетект — кореневий package.json → lang-js
|
|
8
|
+
|
|
3
9
|
## [1.25.1] - 2026-07-19
|
|
4
10
|
|
|
5
11
|
### Fixed
|
package/bin/n-rules.js
CHANGED
|
@@ -1883,7 +1883,22 @@ try {
|
|
|
1883
1883
|
// n-rules taze diff — read-only semver-diff package.json ↔ package.json.taze-bak
|
|
1884
1884
|
// (root + воркспейси) для скілу n-taze: скрипт класифікує major-оновлення,
|
|
1885
1885
|
// агент отримує готовий список замість ручного порівняння бекапів.
|
|
1886
|
-
|
|
1886
|
+
// Живе у плагіні @7n/rules-lang-js (фаза 5a spec lang-plugins-extraction) —
|
|
1887
|
+
// резолвимо його taze-handler і беремо named-експорт runTazeCli.
|
|
1888
|
+
const { getHandlers } = await import('../scripts/lib/resolve-plugins.mjs')
|
|
1889
|
+
const { readNRulesConfigLite } = await import('../scripts/lib/read-n-rules-config-lite.mjs')
|
|
1890
|
+
const config = await readNRulesConfigLite(cwd())
|
|
1891
|
+
const handler = getHandlers(cwd(), config, 'taze').find(h => h.pluginName === '@7n/rules-lang-js')
|
|
1892
|
+
if (!handler) {
|
|
1893
|
+
console.error(
|
|
1894
|
+
'❌ taze diff потребує плагін @7n/rules-lang-js (npm/bun-гілка) — запусти npx @7n/rules для авто-встановлення'
|
|
1895
|
+
)
|
|
1896
|
+
process.exitCode = 1
|
|
1897
|
+
break
|
|
1898
|
+
}
|
|
1899
|
+
const { pathToFileURL } = await import('node:url')
|
|
1900
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
1901
|
+
const { runTazeCli } = await import(pathToFileURL(handler.modulePath).href)
|
|
1887
1902
|
process.exitCode = await runTazeCli(args)
|
|
1888
1903
|
|
|
1889
1904
|
break
|
package/package.json
CHANGED
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: plugin-api.mjs
|
|
4
4
|
resource: npm/scripts/lib/plugin-api.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: c614a880
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
tier: local-min-retry
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: resolve-plugins.mjs
|
|
4
4
|
resource: npm/scripts/lib/resolve-plugins.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: fb58860d
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
|
|
@@ -7,11 +7,40 @@
|
|
|
7
7
|
* модуль-обробник експортує обʼєкт провайдера як `default`. Наступні порти
|
|
8
8
|
* (doc-files, lint) додаються окремими фазами — не проєктуються наперед.
|
|
9
9
|
*
|
|
10
|
-
* Semver-утиліти
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Semver-утиліти (caret-класифікація major/minor) живуть саме тут — єдине
|
|
11
|
+
* джерело правила для всіх мовних плагінів, без імпорту внутрішніх шляхів
|
|
12
|
+
* `@7n/rules` і без циклу plugin-api ↔ плагін.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
// Заякорено на початок (після можливих range-операторів `^~>=<`, пробілів, `v`),
|
|
15
|
+
// щоб НЕ ловити версію всередині protocol-specifier-ів (`workspace:1.0.0`, `npm:x@1.2.3`).
|
|
16
|
+
const SEMVER_RE = /^[\s~^>=<v]*(\d+)\.(\d+)\.(\d+)/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Парсить semver-ядро зі specifier-а (ігнорує range-префікси `^`/`~`/`>=` тощо).
|
|
20
|
+
* @param {string} spec версійний specifier із package.json
|
|
21
|
+
* @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-semver (`workspace:*`, git-url, `*`)
|
|
22
|
+
*/
|
|
23
|
+
export function parseVersion(spec) {
|
|
24
|
+
if (typeof spec !== 'string') return null
|
|
25
|
+
const m = SEMVER_RE.exec(spec)
|
|
26
|
+
if (!m) return null
|
|
27
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Чи є перехід `from → to` breaking за caret-семантикою (змінилась найлівіша
|
|
32
|
+
* ненульова компонента).
|
|
33
|
+
* @param {{major:number,minor:number,patch:number}} from стара версія
|
|
34
|
+
* @param {{major:number,minor:number,patch:number}} to нова версія
|
|
35
|
+
* @returns {boolean} true — major/breaking
|
|
36
|
+
*/
|
|
37
|
+
export function isBreaking(from, to) {
|
|
38
|
+
if (from.major !== to.major) return true
|
|
39
|
+
if (from.major > 0) return false
|
|
40
|
+
if (from.minor !== to.minor) return true
|
|
41
|
+
if (from.minor > 0) return false
|
|
42
|
+
return from.patch !== to.patch
|
|
43
|
+
}
|
|
15
44
|
|
|
16
45
|
/** Версія контракту plugin-api: плагін декларує `requiresPluginApi`, несумісність → skip, не креш. */
|
|
17
46
|
export const PLUGIN_API_VERSION = 1
|
|
@@ -31,11 +31,12 @@ export const KNOWN_CI_PLUGINS = Object.freeze({
|
|
|
31
31
|
/**
|
|
32
32
|
* Відомі мовні плагіни: файловий сигнал екосистеми → npm-пакет. `maxDepth` —
|
|
33
33
|
* до якої глибини шукати сигнал: python — лише корінь (uv-провайдер v1
|
|
34
|
-
* обробляє тільки кореневий pyproject.toml); rust — до 3 рівнів, бо в
|
|
34
|
+
* обробляє тільки кореневий pyproject.toml; js — кореневий package.json); rust — до 3 рівнів, бо в
|
|
35
35
|
* монорепо Cargo.toml часто вкладений (Tauri `app/src-tauri/Cargo.toml`),
|
|
36
36
|
* а провайдер обробляє всі знайдені маніфести.
|
|
37
37
|
*/
|
|
38
38
|
export const KNOWN_LANG_PLUGINS = Object.freeze({
|
|
39
|
+
js: { signal: 'package.json', pkg: '@7n/rules-lang-js', maxDepth: 0 },
|
|
39
40
|
python: { signal: 'pyproject.toml', pkg: '@7n/rules-lang-python', maxDepth: 0 },
|
|
40
41
|
rust: { signal: 'Cargo.toml', pkg: '@7n/rules-lang-rust', maxDepth: 3 }
|
|
41
42
|
})
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -1,144 +1,96 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: n-taze
|
|
3
3
|
description: >-
|
|
4
|
-
Оновлення версій модулів проекту
|
|
4
|
+
Оновлення версій модулів проекту до останніх (усі екосистеми — JS/npm, Rust,
|
|
5
5
|
Python тощо — через активні lang-плагіни) з аналізом major-змін і
|
|
6
6
|
автоматичним рефакторингом несумісного коду
|
|
7
|
-
version: '
|
|
7
|
+
version: '2.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# n-taze — Оновлення версій проекту
|
|
11
11
|
|
|
12
12
|
## Мета
|
|
13
13
|
|
|
14
|
-
Оновити всі модулі проекту
|
|
14
|
+
Оновити всі модулі проекту до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця. Кожна екосистема (JS/npm/bun, Rust/Cargo, Python/uv, …) — гілка відповідного lang-плагіна; конкретні команди гілок описані у фрагментах плагінів наприкінці цього файлу.
|
|
15
15
|
|
|
16
|
-
## Оркестрація
|
|
16
|
+
## Оркестрація — не одним промптом
|
|
17
17
|
|
|
18
18
|
`npx @7n/rules skill pi|cursor|codex taze` **не** передає цей файл одним суцільним
|
|
19
19
|
промптом в один агентський хід — так робив старий дизайн, і саме тому реальний
|
|
20
20
|
прогін міг зависати без жодної діагностики (один величезний непрозорий хід на
|
|
21
21
|
весь монорепо, без проміжних зупинок для перевірки прогресу). Замість цього `npm/skills/taze/js/orchestrate.mjs`:
|
|
22
22
|
|
|
23
|
-
1. Детерміновано, без LLM, виконує кроки 1-3
|
|
24
|
-
2. Для кожного **окремого** major-запису з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `
|
|
23
|
+
1. Детерміновано, без LLM, виконує кроки 1-3 нижче для кожної екосистемної гілки через її EcosystemProvider (detect → backup → bump → diff; конкретні команди — у фрагменті відповідного плагіна наприкінці файлу).
|
|
24
|
+
2. Для кожного **окремого** major-запису з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `promptFor` провайдера, не цей SKILL.md).
|
|
25
25
|
3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Усі екосистеми оркестратор веде через **EcosystemProvider-порт**
|
|
28
28
|
(`@7n/rules/plugin-api`, spec `docs/specs/2026-07-18-lang-plugins-extraction-spec.md`)
|
|
29
29
|
— lang-плагіни доставляються автоматично при першому запуску за файловим
|
|
30
|
-
сигналом (extension-point `taze
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
сигналом (extension-point `taze`; JS/npm — `@7n/rules-lang-js` за кореневим
|
|
31
|
+
`package.json`) і додають свої гілки як фрагменти цього SKILL.md (блок
|
|
32
|
+
наприкінці файлу). Ядро — двигун без мовної специфіки: без сигналу екосистеми
|
|
33
|
+
у звіті немає навіть згадки про неї. Якщо тулчейн гілки не встановлено — вона
|
|
34
|
+
пропускається з причиною у звіті, не блокуючи інші.
|
|
34
35
|
|
|
35
36
|
Переваги: падіння/timeout на одному пакеті/крейті не втрачає прогрес по інших;
|
|
36
37
|
кожен виклик успадковує власний timeout раннера (короткий, бо скоуп малий —
|
|
37
38
|
один запис, не весь монорепо); видно прогрес по записах, а не чорну скриньку.
|
|
38
39
|
|
|
39
|
-
Кроки 1-8 нижче
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
плагінів наприкінці файлу.
|
|
40
|
+
Кроки 1-8 нижче — **скелет** роботи будь-якої гілки (що означає кожен крок);
|
|
41
|
+
конкретні команди кожної екосистеми — у фрагменті її плагіна. Оркестратор
|
|
42
|
+
виконує кроки 1-3/7/8 програмно, 4-6 — як промпт на один запис.
|
|
43
43
|
|
|
44
44
|
## Передумови
|
|
45
45
|
|
|
46
|
-
- Чисте робоче дерево (`git status` без незакомічених змін у
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- Передумови екосистемних гілок (інструменти на кшталт cargo-edit чи uv) — у фрагментах плагінів наприкінці файлу; відсутній тулчейн не блокує інші гілки.
|
|
46
|
+
- Чисте робоче дерево (`git status` без незакомічених змін у маніфестах/lock-файлах залежностей) — інакше різницю не відрізнити від оновлення.
|
|
47
|
+
- Запуск з кореня проекту.
|
|
48
|
+
- Передумови конкретних гілок (bun/bunx, cargo-edit, uv тощо) — у фрагментах плагінів наприкінці файлу; відсутній тулчейн не блокує інші гілки.
|
|
50
49
|
|
|
51
|
-
## Workflow
|
|
50
|
+
## Workflow (скелет — конкретика у фрагментах плагінів)
|
|
52
51
|
|
|
53
52
|
### 1. Зафіксувати стартовий стан
|
|
54
53
|
|
|
55
|
-
Перед оновленням зберегти
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
cp package.json package.json.taze-bak
|
|
59
|
-
cp bun.lock bun.lock.taze-bak
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
(У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видалити в кінці.)
|
|
54
|
+
Перед оновленням зберегти копії маніфестів залежностей (`<manifest>.taze-bak`) — щоб потім детерміновано обчислити, які модулі стрибнули через major. Файли тимчасові, видаляються в кінці.
|
|
63
55
|
|
|
64
56
|
### 2. Запустити оновлення
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
bunx taze -w -r latest
|
|
68
|
-
bun install
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- `-w` — записати нові версії у `package.json`.
|
|
72
|
-
- `-r` — рекурсивно по всіх воркспейсах.
|
|
73
|
-
- `latest` — піднімати навіть major.
|
|
58
|
+
Командою екосистеми підняти всі залежності до останніх версій, **включно з major**, і синхронізувати lock-файл.
|
|
74
59
|
|
|
75
60
|
### 3. Виявити major-оновлення
|
|
76
61
|
|
|
77
|
-
> **Не порівнюй
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
n-rules taze diff
|
|
81
|
-
```
|
|
62
|
+
> **Не порівнюй маніфести вручну.** Класифікацію semver несе детермінований diff гілки (порівняння з `.taze-bak`-бекапом за caret-правилом: major = змінилась найлівіша ненульова компонента — `1.x→2.x`, `0.4.x→0.5.x`, `0.0.3→0.0.4`).
|
|
82
63
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Покриває **прямі** залежності з `package.json` (root + воркспейси). Транзитивні major-стрибки (`bun.lock`) — за потреби переглянь окремо; основний ризик breaking-змін — у прямих.
|
|
64
|
+
Major-записи йдуть у кроки 4–6; лічба сумісних (minor/patch) — у звіт кроку 8. Покриваються **прямі** залежності; транзитивні — за потреби окремо.
|
|
86
65
|
|
|
87
66
|
### 4. Зібрати breaking changes по кожному major-оновленню
|
|
88
67
|
|
|
89
|
-
Для кожного
|
|
90
|
-
|
|
91
|
-
1. **CHANGELOG / Releases репозиторію модуля** — найшвидше і найточніше. Адресу репозиторію взяти з `package.json` модуля у `node_modules/<name>/package.json` (поле `repository`). Дістати релізи між старою і новою версією.
|
|
92
|
-
2. **Git-різниця в `node_modules/<name>`** — якщо CHANGELOG відсутній або неінформативний, порівняти попередню версію (з кешу bun: `~/.bun/install/cache/<name>@<old-version>/`) з новою (`node_modules/<name>/`) через `diff -r` по `dist/` / `*.d.ts` / публічних entry-points з `exports`.
|
|
93
|
-
3. **Якщо немає кешованої старої версії** — встановити її окремо в тимчасову теку (`mkdir -p /tmp/taze-old && cd /tmp/taze-old && bun add <name>@<old-version>`) і порівняти.
|
|
94
|
-
|
|
95
|
-
Цікавлять: видалені/перейменовані експорти, змінені сигнатури функцій, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
|
|
68
|
+
Для кожного запису зі списку зібрати фактичні відмінності: CHANGELOG/Releases репозиторію пакета (джерело адреси — у фрагменті гілки), а без них — diff публічного API між кешованою старою версією і новою. Цікавлять: видалені/перейменовані експорти, змінені сигнатури, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
|
|
96
69
|
|
|
97
70
|
### 5. Перевірити сумісність з кодом проекту
|
|
98
71
|
|
|
99
|
-
Для кожного breaking change знайти
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Класифікувати:
|
|
106
|
-
|
|
107
|
-
- **сумісно** — проект не використовує зачеплене API → нічого не робити.
|
|
108
|
-
- **несумісно** — використання знайдено → перейти до п. 6.
|
|
72
|
+
Для кожного breaking change знайти використання зачепленого API в коді (`rg -n` по мові гілки). Класифікувати: **сумісно** — не використовується → нічого не робити; **несумісно** — перейти до п. 6.
|
|
109
73
|
|
|
110
74
|
### 6. Рефакторинг несумісних місць
|
|
111
75
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
npx @7n/rules lint
|
|
116
|
-
bun run typecheck # якщо є
|
|
117
|
-
bun test # якщо є
|
|
118
|
-
```
|
|
76
|
+
Застосувати міграцію згідно з changelog пакета (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок — прогнати перевірки гілки (лінт/typecheck/тести — конкретні команди у фрагменті). Нетривіальна або неоднозначна міграція — **не вгадувати**: TODO у коді з посиланням на CHANGELOG і запис у звіт як ручна дія.
|
|
119
77
|
|
|
120
78
|
### 7. Прибрати тимчасові файли
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
rm package.json.taze-bak bun.lock.taze-bak
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
(І решту бекапів воркспейсів, якщо створювались.)
|
|
80
|
+
Видалити всі `.taze-bak`-бекапи, створені на кроці 1.
|
|
127
81
|
|
|
128
82
|
### 8. Звіт користувачу
|
|
129
83
|
|
|
130
|
-
|
|
84
|
+
Для кожної обробленої гілки — окрема секція:
|
|
131
85
|
|
|
132
|
-
- **Оновлено (minor/patch):** кількість
|
|
86
|
+
- **Оновлено (minor/patch):** кількість пакетів, без деталей.
|
|
133
87
|
- **Major-оновлення:** список `<name>: <old> → <new>` з посиланням на release notes.
|
|
134
88
|
- **Зрефакторено автоматично:** список файлів і коротко що саме змінено.
|
|
135
89
|
- **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
|
|
136
|
-
- **Стан перевірок:**
|
|
137
|
-
|
|
138
|
-
Для кожної обробленої екосистемної гілки — окрема секція з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання) і власними перевірками (див. фрагменти плагінів наприкінці файлу).
|
|
90
|
+
- **Стан перевірок:** перевірки гілки — pass/fail з номером рядка, де впало.
|
|
139
91
|
|
|
140
92
|
## Примітка
|
|
141
93
|
|
|
142
94
|
- Не запускати `npx @7n/rules lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
|
|
143
95
|
- Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
|
|
144
|
-
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується
|
|
96
|
+
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується пакетів кожної гілки окремо.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: orchestrate.mjs
|
|
4
4
|
resource: npm/skills/taze/js/orchestrate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: df10e67a
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -12,30 +12,27 @@ docgen:
|
|
|
12
12
|
|
|
13
13
|
## Огляд
|
|
14
14
|
|
|
15
|
-
Оркеструє taze
|
|
15
|
+
Оркеструє taze як чистий цикл по EcosystemProvider-ах (фаза 5a spec lang-plugins-extraction: ядро — двигун без мовної специфіки). ВСІ екосистеми, включно з npm/bun (`@7n/rules-lang-js`), приходять з плагінів (extension-point `taze`): для кожного провайдера — detect → available → backup → bump → diff, далі по одному ізольованому виклику раннера на кожен major-запис (`promptFor` провайдера), потім cleanup і композиція Markdown-звіту. Виконує файлові операції і запускає зовнішні команди (git, npx для worktree-бутстрапу, команди провайдерів) — не read-only.
|
|
16
16
|
|
|
17
17
|
## Поведінка
|
|
18
18
|
|
|
19
|
-
- **buildDependencyPrompt** — формує промпт ОДНОГО ітеративного виклику (лише перевірка сумісності й рефакторинг) для одного major-пакета npm/bun.
|
|
20
19
|
- **callRunner** — диспетчер одного ітеративного виклику: `pi` — вбудований pi-агент (текст через `deps.out`), `cursor`/`codex` — napi-міст ACP (`@7n/llm-lib/acp`).
|
|
21
|
-
- **backupWorkspacePackageFiles** / **cleanupBackups** — бекап і прибирання `package.json` кожного воркспейсу.
|
|
22
20
|
- **loadPluginTazeProviders** — завантажує провайдерів з активних плагінів: `.n-rules.json`/автодетект → `resolvePlugins` (плагін доставляється автоматично при першому запуску) → handler-модулі extension-point `taze` → валідація `assertEcosystemProvider`; битий плагін — warning і пропуск, не провал.
|
|
23
|
-
- **formatReport** —
|
|
24
|
-
- **runTazeOrchestrator** — повний прогін:
|
|
21
|
+
- **formatReport** — Markdown-звіт: рівноправна секція на кожну екосистему з manifests (без manifests — тиша) + загальний підсумок; без окремого LLM-виклику.
|
|
22
|
+
- **runTazeOrchestrator** — повний прогін: гарантує ізольований worktree (авто-створює `<branch>-taze` через `npx @7n/mt worktree create`, після завершення переносить зміни назад і прибирає авто-створене дерево), проганяє кожного провайдера наскрізь і повертає `{ ok, report, ecosystems }`. Без жодного taze-провайдера — попередження (для npm/bun-гілки потрібен `@7n/rules-lang-js`).
|
|
23
|
+
- **bringChangesBackToOriginal** / **removeAutoCreatedWorktree** — перенесення незакомічених змін з авто-створеного worktree у вихідне дерево і його прибирання.
|
|
25
24
|
|
|
26
25
|
## Публічний API
|
|
27
26
|
|
|
28
|
-
- buildDependencyPrompt — промпт одного npm-major-запису.
|
|
29
27
|
- callRunner — виклик обраного раннера (`pi`/`cursor`/`codex`) з одним промптом.
|
|
30
|
-
- backupWorkspacePackageFiles / cleanupBackups — бекап/прибирання `package.json` воркспейсів.
|
|
31
28
|
- loadPluginTazeProviders — валідні EcosystemProvider-и з handler-модулів плагінів.
|
|
32
|
-
- formatReport — фінальний Markdown-звіт із
|
|
29
|
+
- formatReport — фінальний Markdown-звіт із записів екосистем.
|
|
33
30
|
- runTazeOrchestrator — повна оркестрація taze; `deps.ecosystemProviders` повністю замінює список провайдерів (для тестів).
|
|
31
|
+
- bringChangesBackToOriginal / removeAutoCreatedWorktree — життєвий цикл авто-створеного worktree.
|
|
34
32
|
|
|
35
33
|
## Гарантії поведінки
|
|
36
34
|
|
|
37
|
-
- Виконує файлові операції
|
|
38
|
-
-
|
|
39
|
-
- npm/bun-гілка активна лише за кореневим package.json: на чисто-Python/Rust репо вона тихо пропускається (лог + без npm-рядків у звіті), а не валить прогін через `bun install → exit 1`.
|
|
35
|
+
- Виконує файлові операції і запускає зовнішні команди — НЕ read-only.
|
|
36
|
+
- Кроки оркестрації виконуються лише в ізольованому worktree; авто-створене дерево прибирається у `finally` навіть при винятку, зміни переносяться назад.
|
|
40
37
|
- Виняток усередині одного провайдера (bump/diff/команда) не зупиняє інших — фіксується в `error` запису екосистеми й у звіті; `ok` результату тоді false.
|
|
41
38
|
- Падіння одного пакета в ізольованому виклику раннера не втрачає прогрес по інших записах.
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/** @see ./docs/orchestrate.md */
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
4
|
-
import { copyFile, rm } from 'node:fs/promises'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
3
|
import { pathToFileURL } from 'node:url'
|
|
7
4
|
|
|
8
5
|
import {
|
|
@@ -13,38 +10,9 @@ import {
|
|
|
13
10
|
import { assertEcosystemProvider } from '../../../scripts/lib/plugin-api.mjs'
|
|
14
11
|
import { readNRulesConfigLite } from '../../../scripts/lib/read-n-rules-config-lite.mjs'
|
|
15
12
|
import { getHandlers, resolvePlugins } from '../../../scripts/lib/resolve-plugins.mjs'
|
|
16
|
-
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
17
|
-
import { collectTazeDiff } from './diff.mjs'
|
|
18
13
|
|
|
19
14
|
export { bringChangesBackToOriginal, removeAutoCreatedWorktree } from '../../../scripts/lib/auto-worktree.mjs'
|
|
20
15
|
|
|
21
|
-
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
22
|
-
const BACKUP_SUFFIX = '.taze-bak'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Промпт ОДНОГО ітеративного виклику — лише кроки 4-6 SKILL.md (breaking
|
|
26
|
-
* changes → сумісність коду → рефакторинг) для ОДНОГО major-пакета. Кроки
|
|
27
|
-
* 1-3/7/8 виконує оркестратор детерміновано, без LLM.
|
|
28
|
-
* @param {{workspace: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectTazeDiff`)
|
|
29
|
-
* @returns {string} готовий промпт
|
|
30
|
-
*/
|
|
31
|
-
export function buildDependencyPrompt({ workspace, pkg, from, to }) {
|
|
32
|
-
return [
|
|
33
|
-
'# Major-оновлення одного пакета: перевірка сумісності й рефакторинг',
|
|
34
|
-
'',
|
|
35
|
-
`Пакет \`${pkg}\` у воркспейсі \`${workspace}\`: **${from} → ${to}** — вже застосовано в package.json/bun.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
36
|
-
'',
|
|
37
|
-
'## Кроки',
|
|
38
|
-
`1. Зібрати breaking changes цього оновлення: CHANGELOG/Releases репозиторію модуля (поле \`repository\` у \`node_modules/${pkg}/package.json\`), або git/diff між закешованою старою версією (\`~/.bun/install/cache/${pkg}@<стара-версія>/\`) і новою (\`node_modules/${pkg}/\`).`,
|
|
39
|
-
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n\` по імпортах/викликах \`${pkg}\`).`,
|
|
40
|
-
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом).',
|
|
41
|
-
'4. Якщо були правки — запусти `npx @7n/rules lint`, typecheck/test якщо є в проєкті.',
|
|
42
|
-
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
43
|
-
'',
|
|
44
|
-
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
45
|
-
].join('\n')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
16
|
/**
|
|
49
17
|
* Диспетчер одного ітеративного виклику на обраний раннер. `pi` — вбудований
|
|
50
18
|
* pi-агент (`@7n/llm-lib/agent-skill`; текст перехоплюється через `deps.out`,
|
|
@@ -87,58 +55,6 @@ export async function callRunner(runner, prompt, cwd, deps = {}) {
|
|
|
87
55
|
}
|
|
88
56
|
}
|
|
89
57
|
|
|
90
|
-
/**
|
|
91
|
-
* Синхронно виконує детерміновану команду (bunx/bun), кидає з
|
|
92
|
-
* exit-кодом+stderr при провалі.
|
|
93
|
-
* @param {string} cmd бінарник
|
|
94
|
-
* @param {string[]} args аргументи
|
|
95
|
-
* @param {string} cwd робочий каталог
|
|
96
|
-
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
97
|
-
* @returns {string} stdout
|
|
98
|
-
*/
|
|
99
|
-
function runCommand(cmd, args, cwd, spawnFn) {
|
|
100
|
-
const result = spawnFn(cmd, args, { cwd, encoding: 'utf8' })
|
|
101
|
-
if (result.status !== 0) {
|
|
102
|
-
throw new Error(`${cmd} ${args.join(' ')} → exit ${result.status}: ${result.stderr || result.stdout}`)
|
|
103
|
-
}
|
|
104
|
-
return result.stdout
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Бекапить package.json кожного воркспейсу (крок 1 SKILL.md) — потрібно для
|
|
109
|
-
* класифікації major/minor через `collectTazeDiff` після bump-у.
|
|
110
|
-
* @param {string} cwd корінь репо
|
|
111
|
-
* @param {{ getMonorepoPackageRootDirs?: (cwd: string) => Promise<string[]>, copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжекти
|
|
112
|
-
* @returns {Promise<string[]>} відносні шляхи воркспейсів, що мали package.json
|
|
113
|
-
*/
|
|
114
|
-
export async function backupWorkspacePackageFiles(cwd, deps = {}) {
|
|
115
|
-
const getRoots = deps.getMonorepoPackageRootDirs ?? getMonorepoPackageRootDirs
|
|
116
|
-
const copy = deps.copyFile ?? copyFile
|
|
117
|
-
const roots = await getRoots(cwd)
|
|
118
|
-
const backedUp = []
|
|
119
|
-
for (const ws of roots) {
|
|
120
|
-
const pkgPath = join(cwd, ws, 'package.json')
|
|
121
|
-
if (!existsSync(pkgPath)) continue
|
|
122
|
-
await copy(pkgPath, `${pkgPath}${BACKUP_SUFFIX}`)
|
|
123
|
-
backedUp.push(ws)
|
|
124
|
-
}
|
|
125
|
-
return backedUp
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Прибирає бекапи package.json після завершення (крок 7 SKILL.md).
|
|
130
|
-
* @param {string} cwd корінь репо
|
|
131
|
-
* @param {string[]} workspaces воркспейси з бекапом (з `backupWorkspacePackageFiles`)
|
|
132
|
-
* @param {{ rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжект
|
|
133
|
-
* @returns {Promise<void>}
|
|
134
|
-
*/
|
|
135
|
-
export async function cleanupBackups(cwd, workspaces, deps = {}) {
|
|
136
|
-
const remove = deps.rm ?? rm
|
|
137
|
-
for (const ws of workspaces) {
|
|
138
|
-
await remove(join(cwd, ws, `package.json${BACKUP_SUFFIX}`), { force: true })
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
58
|
/**
|
|
143
59
|
* Завантажує EcosystemProvider-и з активних плагінів проєкту: `.n-rules.json`
|
|
144
60
|
* (або автодетект за файловими сигналами — `pyproject.toml` → lang-python) →
|
|
@@ -265,50 +181,39 @@ function appendEcosystemSection(lines, eco) {
|
|
|
265
181
|
|
|
266
182
|
/**
|
|
267
183
|
* Компонує підсумковий звіт (крок 8 SKILL.md) детерміновано з результатів
|
|
268
|
-
* ітерацій — без окремого LLM-виклику для самого звіту.
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* results: Array<{pkg:string, workspace:string, from:string, to:string, ok:boolean, error:string|null}>,
|
|
273
|
-
* ecosystems?: Array<object>,
|
|
274
|
-
* npmPresent?: boolean
|
|
275
|
-
* }} args дані звіту (`ecosystems` — записи з `runEcosystem`, по одному на провайдера;
|
|
276
|
-
* `npmPresent: false` — репо без кореневого package.json, npm-рядки не друкуються)
|
|
184
|
+
* ітерацій — без окремого LLM-виклику для самого звіту. Усі екосистеми
|
|
185
|
+
* (включно з npm/bun — плагін `@7n/rules-lang-js`, фаза 5a) — рівноправні
|
|
186
|
+
* секції; екосистема без manifests — тиша.
|
|
187
|
+
* @param {{ ecosystems?: Array<object> }} args записи з `runEcosystem`, по одному на провайдера
|
|
277
188
|
* @returns {string} markdown-звіт
|
|
278
189
|
*/
|
|
279
|
-
export function formatReport({
|
|
280
|
-
const lines = ['## taze: підсумок'
|
|
281
|
-
if (npmPresent) {
|
|
282
|
-
lines.push(`- **Оновлено (minor/patch):** ${minorPatch}`, `- **Major-оновлення:** ${results.length}`)
|
|
283
|
-
for (const r of results) {
|
|
284
|
-
lines.push(formatResultLine(r, r.workspace))
|
|
285
|
-
}
|
|
286
|
-
}
|
|
190
|
+
export function formatReport({ ecosystems = [] }) {
|
|
191
|
+
const lines = ['## taze: підсумок']
|
|
287
192
|
|
|
288
|
-
let
|
|
193
|
+
let total = 0
|
|
289
194
|
for (const eco of ecosystems) {
|
|
290
|
-
|
|
195
|
+
total += appendEcosystemSection(lines, eco)
|
|
291
196
|
}
|
|
292
197
|
|
|
293
|
-
lines.push('', `- **Всього змінено:** ${
|
|
198
|
+
lines.push('', `- **Всього змінено:** ${total}`)
|
|
294
199
|
return lines.join('\n')
|
|
295
200
|
}
|
|
296
201
|
|
|
297
202
|
/**
|
|
298
|
-
* Оркеструє taze:
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* (
|
|
304
|
-
*
|
|
203
|
+
* Оркеструє taze: чистий цикл по EcosystemProvider-ах (кроки 1-3/7/8 —
|
|
204
|
+
* детерміновано в провайдері, кроки 4-6 — по одному ізольованому виклику
|
|
205
|
+
* `callRunner` на кожен major-запис) замість одного величезного непрозорого
|
|
206
|
+
* ходу на весь монорепо. Ядро — двигун без мовної специфіки (фаза 5a spec):
|
|
207
|
+
* ВСІ екосистеми, включно з npm/bun (`@7n/rules-lang-js`), приходять з
|
|
208
|
+
* плагінів (extension-point `taze`). Падіння одного пакета/однієї екосистеми
|
|
209
|
+
* не втрачає прогрес по інших.
|
|
305
210
|
* @param {{
|
|
306
211
|
* cwd?: string,
|
|
307
212
|
* runner?: 'pi' | 'cursor' | 'codex',
|
|
308
213
|
* log?: (line: string) => void,
|
|
309
|
-
* deps?: { spawnFn?: typeof spawnSync,
|
|
214
|
+
* deps?: { spawnFn?: typeof spawnSync, callRunner?: (runner: string, prompt: string, cwd: string, deps: object) => Promise<{ok: boolean, text: string, error: string|null}>, ecosystemProviders?: object[] } & Record<string, unknown>
|
|
310
215
|
* }} [options] опції + інжекти для тестів (`deps.ecosystemProviders` повністю замінює список провайдерів)
|
|
311
|
-
* @returns {Promise<{ ok: boolean, report: string,
|
|
216
|
+
* @returns {Promise<{ ok: boolean, report: string, ecosystems: Array<object> }>} результат
|
|
312
217
|
*/
|
|
313
218
|
export async function runTazeOrchestrator(options = {}) {
|
|
314
219
|
const runner = options.runner ?? 'pi'
|
|
@@ -325,55 +230,22 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
325
230
|
const cwd = worktree.cwd
|
|
326
231
|
|
|
327
232
|
try {
|
|
328
|
-
// npm/bun-гілка активна лише за кореневим package.json — на чисто-Python/Rust
|
|
329
|
-
// репо `bun install` падає з exit 1, і без цього гейта весь прогін гинув би
|
|
330
|
-
// до екосистемних провайдерів. Той самий принцип «тиші», що й для мовних
|
|
331
|
-
// екосистем: немає сигналу — немає ані кроків, ані згадки у звіті.
|
|
332
|
-
const npmPresent = existsSync(join(cwd, 'package.json'))
|
|
333
|
-
let diff = { major: [], minorPatch: 0, totalChanged: 0 }
|
|
334
|
-
const results = []
|
|
335
|
-
if (npmPresent) {
|
|
336
|
-
log('📦 Бекап package.json...')
|
|
337
|
-
const backedUpWorkspaces = await backupWorkspacePackageFiles(cwd, deps)
|
|
338
|
-
|
|
339
|
-
log('⬆️ bunx taze -w -r latest...')
|
|
340
|
-
runCommand('bunx', ['taze', '-w', '-r', 'latest'], cwd, spawnFn)
|
|
341
|
-
log('📥 bun install...')
|
|
342
|
-
runCommand('bun', ['install'], cwd, spawnFn)
|
|
343
|
-
|
|
344
|
-
const collectDiff = deps.collectTazeDiff ?? collectTazeDiff
|
|
345
|
-
diff = await collectDiff(cwd)
|
|
346
|
-
log(`🔍 diff: ${diff.major.length} major, ${diff.minorPatch} minor/patch`)
|
|
347
|
-
|
|
348
|
-
for (const entry of diff.major) {
|
|
349
|
-
log(`🔧 ${entry.pkg} (${entry.workspace}): ${entry.from} → ${entry.to}...`)
|
|
350
|
-
const outcome = await call(runner, buildDependencyPrompt(entry), cwd, deps)
|
|
351
|
-
results.push({ ...entry, ...outcome })
|
|
352
|
-
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
await cleanupBackups(cwd, backedUpWorkspaces, deps)
|
|
356
|
-
} else {
|
|
357
|
-
log('⏭ npm/bun: кореневого package.json немає — гілка пропущена')
|
|
358
|
-
}
|
|
359
|
-
|
|
360
233
|
const providers = deps.ecosystemProviders ?? (await loadPluginTazeProviders(cwd, log, deps))
|
|
234
|
+
if (providers.length === 0) {
|
|
235
|
+
log(
|
|
236
|
+
'⏭ Жодного taze-провайдера: жоден активний плагін не надає extension-point `taze` (для npm/bun-гілки потрібен @7n/rules-lang-js)'
|
|
237
|
+
)
|
|
238
|
+
}
|
|
361
239
|
const ecosystems = []
|
|
362
240
|
for (const provider of providers) {
|
|
363
241
|
ecosystems.push(await runEcosystem(provider, { cwd, runner, log, deps, spawnFn, call }))
|
|
364
242
|
}
|
|
365
243
|
|
|
366
|
-
const report = formatReport({
|
|
367
|
-
minorPatch: diff.minorPatch,
|
|
368
|
-
totalChanged: diff.totalChanged,
|
|
369
|
-
results,
|
|
370
|
-
ecosystems,
|
|
371
|
-
npmPresent
|
|
372
|
-
})
|
|
244
|
+
const report = formatReport({ ecosystems })
|
|
373
245
|
log(report)
|
|
374
246
|
|
|
375
247
|
const ecosystemsOk = ecosystems.every(eco => eco.error === null && eco.results.every(r => r.ok))
|
|
376
|
-
return { ok:
|
|
248
|
+
return { ok: ecosystemsOk, report, ecosystems }
|
|
377
249
|
} finally {
|
|
378
250
|
// Лише для АВТОстворених worktree — якщо викликач уже сидів у своєму
|
|
379
251
|
// worktree (worktree.autoCreated === false), це не наш worktree і не
|
package/skills/taze/js/diff.mjs
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/diff.md */
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
7
|
-
|
|
8
|
-
/** Поля package.json із залежностями, які порівнюємо. */
|
|
9
|
-
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
10
|
-
|
|
11
|
-
/** Дефолтний суфікс бекапу, який створює крок 1 скіла (`cp package.json package.json.taze-bak`). */
|
|
12
|
-
const DEFAULT_BACKUP_SUFFIX = '.taze-bak'
|
|
13
|
-
|
|
14
|
-
// Заякорено на початок (після можливих range-операторів `^~>=<`, пробілів, `v`),
|
|
15
|
-
// щоб НЕ ловити версію всередині protocol-specifier-ів (`workspace:1.0.0`, `npm:x@1.2.3`).
|
|
16
|
-
const SEMVER_RE = /^[\s~^>=<v]*(\d+)\.(\d+)\.(\d+)/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Парсить semver-ядро зі specifier-а (ігнорує range-префікси `^`/`~`/`>=` тощо).
|
|
20
|
-
* @param {string} spec версійний specifier із package.json
|
|
21
|
-
* @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-semver (`workspace:*`, git-url, `*`)
|
|
22
|
-
*/
|
|
23
|
-
export function parseVersion(spec) {
|
|
24
|
-
if (typeof spec !== 'string') return null
|
|
25
|
-
const m = SEMVER_RE.exec(spec)
|
|
26
|
-
if (!m) return null
|
|
27
|
-
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Чи є перехід `from → to` breaking за caret-семантикою (змінилась найлівіша
|
|
32
|
-
* ненульова компонента).
|
|
33
|
-
* @param {{major:number,minor:number,patch:number}} from стара версія
|
|
34
|
-
* @param {{major:number,minor:number,patch:number}} to нова версія
|
|
35
|
-
* @returns {boolean} true — major/breaking
|
|
36
|
-
*/
|
|
37
|
-
export function isBreaking(from, to) {
|
|
38
|
-
if (from.major !== to.major) return true
|
|
39
|
-
if (from.major > 0) return false
|
|
40
|
-
if (from.minor !== to.minor) return true
|
|
41
|
-
if (from.minor > 0) return false
|
|
42
|
-
return from.patch !== to.patch
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Порівнює два package.json-обʼєкти й повертає зміни залежностей.
|
|
47
|
-
* @param {object} oldPkg розпарсений старий package.json (бекап)
|
|
48
|
-
* @param {object} newPkg розпарсений новий package.json
|
|
49
|
-
* @param {string} workspace мітка воркспейсу (`.` для кореня)
|
|
50
|
-
* @returns {{major: Array<{workspace:string, pkg:string, from:string, to:string}>, minorPatch:number}} зміни
|
|
51
|
-
*/
|
|
52
|
-
export function diffPackageJson(oldPkg, newPkg, workspace) {
|
|
53
|
-
const major = []
|
|
54
|
-
let minorPatch = 0
|
|
55
|
-
for (const field of DEP_FIELDS) {
|
|
56
|
-
const oldDeps = oldPkg?.[field]
|
|
57
|
-
const newDeps = newPkg?.[field]
|
|
58
|
-
if (!oldDeps || !newDeps) continue
|
|
59
|
-
for (const [pkg, from] of Object.entries(oldDeps)) {
|
|
60
|
-
const to = newDeps[pkg]
|
|
61
|
-
if (to === undefined || to === from) continue
|
|
62
|
-
const fromV = parseVersion(from)
|
|
63
|
-
const toV = parseVersion(to)
|
|
64
|
-
if (fromV && toV && isBreaking(fromV, toV)) {
|
|
65
|
-
major.push({ workspace, pkg, from, to })
|
|
66
|
-
} else {
|
|
67
|
-
minorPatch += 1
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return { major, minorPatch }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Читає JSON-файл або повертає null, якщо файл відсутній / невалідний.
|
|
76
|
-
* @param {string} path абсолютний шлях
|
|
77
|
-
* @returns {Promise<object|null>} розпарсений обʼєкт або null
|
|
78
|
-
*/
|
|
79
|
-
async function readJsonOrNull(path) {
|
|
80
|
-
if (!existsSync(path)) return null
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(await readFile(path, 'utf8'))
|
|
83
|
-
} catch {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Збирає diff по всьому монорепо: для кожного воркспейсу порівнює
|
|
90
|
-
* `<ws>/package.json` з `<ws>/package.json<backupSuffix>`.
|
|
91
|
-
* @param {string} cwd корінь репозиторію
|
|
92
|
-
* @param {string} [backupSuffix] суфікс бекап-файлу
|
|
93
|
-
* @returns {Promise<{major: Array<{workspace:string, pkg:string, from:string, to:string}>, minorPatch:number, totalChanged:number, comparedWorkspaces:number}>} агрегований diff
|
|
94
|
-
*/
|
|
95
|
-
export async function collectTazeDiff(cwd, backupSuffix = DEFAULT_BACKUP_SUFFIX) {
|
|
96
|
-
const roots = await getMonorepoPackageRootDirs(cwd)
|
|
97
|
-
const major = []
|
|
98
|
-
let minorPatch = 0
|
|
99
|
-
let comparedWorkspaces = 0
|
|
100
|
-
for (const ws of roots) {
|
|
101
|
-
const dir = join(cwd, ws)
|
|
102
|
-
const oldPkg = await readJsonOrNull(join(dir, `package.json${backupSuffix}`))
|
|
103
|
-
const newPkg = await readJsonOrNull(join(dir, 'package.json'))
|
|
104
|
-
if (!oldPkg || !newPkg) continue
|
|
105
|
-
comparedWorkspaces += 1
|
|
106
|
-
const res = diffPackageJson(oldPkg, newPkg, ws)
|
|
107
|
-
major.push(...res.major)
|
|
108
|
-
minorPatch += res.minorPatch
|
|
109
|
-
}
|
|
110
|
-
return { major, minorPatch, totalChanged: major.length + minorPatch, comparedWorkspaces }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const USAGE = 'Usage: n-rules taze diff [--backup-suffix <suffix>]'
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* CLI: `n-rules taze diff` друкує компактний JSON зі списком major-оновлень і
|
|
117
|
-
* лічбою minor/patch. Read-only.
|
|
118
|
-
* @param {string[]} args аргументи після `taze`
|
|
119
|
-
* @param {string} [cwd] корінь репозиторію (ін'єкція для тестів)
|
|
120
|
-
* @returns {Promise<number>} exit code
|
|
121
|
-
*/
|
|
122
|
-
export async function runTazeCli(args, cwd = process.cwd()) {
|
|
123
|
-
if (args[0] !== 'diff') {
|
|
124
|
-
console.error(USAGE)
|
|
125
|
-
return 1
|
|
126
|
-
}
|
|
127
|
-
const flagAt = args.indexOf('--backup-suffix')
|
|
128
|
-
const backupSuffix = flagAt === -1 ? DEFAULT_BACKUP_SUFFIX : args[flagAt + 1]
|
|
129
|
-
if (!backupSuffix) {
|
|
130
|
-
console.error(USAGE)
|
|
131
|
-
return 1
|
|
132
|
-
}
|
|
133
|
-
const diff = await collectTazeDiff(cwd, backupSuffix)
|
|
134
|
-
if (diff.comparedWorkspaces === 0) {
|
|
135
|
-
console.error(`✗ Не знайдено жодного package.json${backupSuffix} — спершу зроби бекап (крок 1 скіла).`)
|
|
136
|
-
return 1
|
|
137
|
-
}
|
|
138
|
-
process.stdout.write(`${JSON.stringify(diff)}\n`)
|
|
139
|
-
return 0
|
|
140
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: diff.mjs
|
|
4
|
-
resource: npm/skills/taze/js/diff.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: a70297a7
|
|
7
|
-
score: 100
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
parseVersion
|
|
11
|
-
Парсить версію з specifier.
|
|
12
|
-
|
|
13
|
-
isBreaking
|
|
14
|
-
Перевіряє перехід версій згідно з caret-семантикою.
|
|
15
|
-
|
|
16
|
-
diffPackageJson
|
|
17
|
-
Порівнює об'єкти package.json і повертає зміни залежностей.
|
|
18
|
-
|
|
19
|
-
collectTazeDiff
|
|
20
|
-
Збирає diff по всьому монорепо порівнюючи бекап та новий файл.
|
|
21
|
-
|
|
22
|
-
runTazeCli
|
|
23
|
-
Друкує результат diff у форматі JSON.
|
|
24
|
-
|
|
25
|
-
## Поведінка
|
|
26
|
-
|
|
27
|
-
parseVersion
|
|
28
|
-
Парсить версію з specifier
|
|
29
|
-
|
|
30
|
-
isBreaking
|
|
31
|
-
Перевіряє перехід версій згідно з caret-семантикою
|
|
32
|
-
|
|
33
|
-
diffPackageJson
|
|
34
|
-
Порівнює об'єкти package.json і повертає зміни залежностей
|
|
35
|
-
|
|
36
|
-
collectTazeDiff
|
|
37
|
-
Збирає diff по всьому монорепо порівнюючи бекап та новий файл
|
|
38
|
-
|
|
39
|
-
runTazeCli
|
|
40
|
-
Друкує результат diff у форматі JSON
|
|
41
|
-
|
|
42
|
-
## Публічний API
|
|
43
|
-
|
|
44
|
-
parseVersion — витягує версію з specifier-а, ігноруючи range-префікси.
|
|
45
|
-
isBreaking — визначає, чи є перехід між версіями breaking за caret-семантикою (зміна найлівішої ненульової частини).
|
|
46
|
-
diffPackageJson — порівнює два package.json-об'єкти та генерує список змін залежностей.
|
|
47
|
-
collectTazeDiff — збирає різницю між версіями всього монорепо, порівнюючи `package.json` з його резервною копією у кожному воркспейсі.
|
|
48
|
-
runTazeCli — виконує команду `n-rules taze diff` для виведення компактного JSON зі списком major-оновлень та лічилки minor/patch.
|
|
49
|
-
|
|
50
|
-
## Гарантії поведінки
|
|
51
|
-
|
|
52
|
-
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
53
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
54
|
-
- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
|
|
55
|
-
- Не звертається до мережі.
|