@7n/rules 1.8.4 → 1.9.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/package.json +1 -1
- package/scripts/docs/skills-cli.md +1 -1
- package/scripts/skills-cli.mjs +42 -0
- package/skills/taze/SKILL.md +21 -0
- package/skills/taze/js/docs/index.md +4 -5
- package/skills/taze/js/docs/orchestrate.md +44 -0
- package/skills/taze/js/orchestrate.mjs +272 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `skill pi/cursor/codex taze` тепер виконується через оркестратор (`npm/skills/taze/js/orchestrate.mjs`) замість одного величезного непрозорого ходу на весь монорепо: детерміновані кроки (бекап/масовий bump/diff/прибирання) без LLM, і по одному ізольованому, обмеженому виклику раннера на кожен major-пакет — падіння/timeout одного пакета не втрачає прогрес по інших
|
|
8
|
+
|
|
3
9
|
## [1.8.4] - 2026-07-17
|
|
4
10
|
|
|
5
11
|
### Fixed
|
package/package.json
CHANGED
package/scripts/skills-cli.mjs
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* (napi-міст до `llm_cascade::acp`, без власного JSON-RPC у JS); deprecated
|
|
9
9
|
* `claude` — окремий JS-шим (`./lib/acp-runner.mjs`), бо Rust-крейт його не моделює.
|
|
10
10
|
*
|
|
11
|
+
* `skill <runner> taze` — виняток із загального шляху "весь SKILL.md одним промптом":
|
|
12
|
+
* делегує в `../skills/taze/js/orchestrate.mjs`, який детерміновано (без LLM) робить
|
|
13
|
+
* бекап/масовий bump/diff/прибирання і лише по одному ОБМЕЖЕНОМУ виклику `<runner>`
|
|
14
|
+
* на кожен major-пакет — замість одного величезного непрозорого ходу на весь монорепо
|
|
15
|
+
* (той, single-shot, раніше зависав без діагностики; per-пакет виклики успадковують
|
|
16
|
+
* власний timeout раннера, і падіння одного пакета не втрачає прогрес по інших).
|
|
17
|
+
*
|
|
11
18
|
* Підтримувані формати:
|
|
12
19
|
* `npx \@7n/rules skill list`
|
|
13
20
|
* `npx \@7n/rules skill taze`
|
|
@@ -202,6 +209,32 @@ async function runLlmCli(kind, prompt, projectDir, logError, deps = {}) {
|
|
|
202
209
|
}
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Виконує `taze` через оркестратор (`../skills/taze/js/orchestrate.mjs`) замість
|
|
214
|
+
* загального одноходового шляху — детерміновані кроки без LLM (бекап/bump/diff/
|
|
215
|
+
* прибирання) + по одному обмеженому виклику обраного `runner` на кожен major-пакет.
|
|
216
|
+
* @param {'pi' | 'cursor' | 'codex'} runner раннер для per-пакетних викликів
|
|
217
|
+
* @param {string} projectDir корінь проєкту (де лежить package.json)
|
|
218
|
+
* @param {(line: string) => void} log вивід прогресу/звіту
|
|
219
|
+
* @param {(line: string) => void} logError вивід помилок
|
|
220
|
+
* @param {{ runTazeOrchestrator?: (opts: object) => Promise<{ ok: boolean, report: string }> }} [deps] інжект для тестів
|
|
221
|
+
* @returns {Promise<number>} exit code (0 — усі major-пакети ok)
|
|
222
|
+
*/
|
|
223
|
+
async function runTazeOrchestratorCli(runner, projectDir, log, logError, deps = {}) {
|
|
224
|
+
let orchestrate = deps.runTazeOrchestrator
|
|
225
|
+
if (!orchestrate) {
|
|
226
|
+
const orchestrateModule = await import('../skills/taze/js/orchestrate.mjs')
|
|
227
|
+
orchestrate = orchestrateModule.runTazeOrchestrator
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const result = await orchestrate({ cwd: projectDir, runner, log, deps })
|
|
231
|
+
return result.ok ? 0 : 1
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logError(error instanceof Error ? error.message : String(error))
|
|
234
|
+
return 1
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
/**
|
|
206
239
|
* Корінь пакета `@7n/rules` (каталог з `skills/`, `rules/`, …).
|
|
207
240
|
* @param {string} [fromModuleUrl] для тестів — `import.meta.url`, відносно якого шукати корінь
|
|
@@ -245,6 +278,15 @@ export async function runSkillsCli(argv, options = {}) {
|
|
|
245
278
|
if (!second) {
|
|
246
279
|
throw new Error(`Skill name is required after "${first}"`)
|
|
247
280
|
}
|
|
281
|
+
if (first !== 'claude' && normalizeSkillId(second) === 'taze') {
|
|
282
|
+
return await runTazeOrchestratorCli(
|
|
283
|
+
/** @type {'pi' | 'cursor' | 'codex'} */ (first),
|
|
284
|
+
projectDir,
|
|
285
|
+
log,
|
|
286
|
+
logError,
|
|
287
|
+
deps
|
|
288
|
+
)
|
|
289
|
+
}
|
|
248
290
|
const task = rest.join(' ')
|
|
249
291
|
const prompt = buildSkillPrompt(skillsRoot, second, task, projectDir)
|
|
250
292
|
if (first === 'pi') {
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -13,6 +13,27 @@ version: '1.1'
|
|
|
13
13
|
|
|
14
14
|
Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml` — і Rust-крейти) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
|
|
15
15
|
|
|
16
|
+
## Оркестрація (npm/bun-гілка) — не одним промптом
|
|
17
|
+
|
|
18
|
+
`npx @7n/rules skill pi|cursor|codex taze` **не** передає цей файл одним суцільним
|
|
19
|
+
промптом в один агентський хід — так робив старий дизайн, і саме тому реальний
|
|
20
|
+
прогін міг зависати без жодної діагностики (один величезний непрозорий хід на
|
|
21
|
+
весь монорепо, без проміжних зупинок для перевірки прогресу). Замість цього `npm/skills/taze/js/orchestrate.mjs`:
|
|
22
|
+
|
|
23
|
+
1. Детерміновано, без LLM, виконує кроки 1-3 нижче (бекап → `bunx taze -w -r latest` → `bun install` → `n-rules taze diff`).
|
|
24
|
+
2. Для кожного **окремого** major-пакета з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього пакета; промпт генерує `buildDependencyPrompt`, не цей SKILL.md).
|
|
25
|
+
3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
|
|
26
|
+
|
|
27
|
+
Переваги: падіння/timeout на одному пакеті не втрачає прогрес по інших; кожен
|
|
28
|
+
виклик успадковує власний timeout раннера (короткий, бо скоуп малий — один
|
|
29
|
+
пакет, не весь монорепо); видно прогрес по пакетах, а не чорну скриньку.
|
|
30
|
+
|
|
31
|
+
Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи (що саме робить
|
|
32
|
+
кожен крок) — оркестратор їх виконує програмно (1-3/7/8) або як промпт на
|
|
33
|
+
один пакет (4-6). Пряме читання цього файлу одним агентом (як SKILL.md для
|
|
34
|
+
інших скілів) досі стосується лише **Rust-гілки** (нижче) — вона оркестратором
|
|
35
|
+
не покрита.
|
|
36
|
+
|
|
16
37
|
## Передумови
|
|
17
38
|
|
|
18
39
|
- Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
|
|
@@ -4,8 +4,7 @@ title: npm/skills/taze/js
|
|
|
4
4
|
resource: npm/skills/taze/js/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
| [diff.mjs](diff.md) | JS Module |
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| --------------------------------- | --------- |
|
|
9
|
+
| [diff.mjs](diff.md) | JS Module |
|
|
10
|
+
| [orchestrate.mjs](orchestrate.md) | JS Module |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: orchestrate.mjs
|
|
4
|
+
resource: npm/skills/taze/js/orchestrate.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 37e2a3e9
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл об’єднує публічні дії `buildDependencyPrompt`, `callRunner`, `backupWorkspacePackageFiles`, `cleanupBackups`, `findCargoManifests`, `formatReport`, `runTazeOrchestrator`, щоб узгодити оновлення залежностей за даними з `main.json` і `package.json`. Він працює read-only: не пише у ФС/БД, має кешування в межах прогону, свідомо пропускає шляхи `node_modules`, тимчасово зберігає бекапи `package.json` у воркспейсах і прибирає їх після завершення. Результат проходу оформлюється через `formatReport` як підсумок змін і стану оновлення.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- **buildDependencyPrompt** — формує текст завдання для перевірки major-оновлення одного пакета й подальшого сумісного рефакторингу.
|
|
21
|
+
- **callRunner** — запускає один ітеративний LLM-виклик у вибраному раннері та повертає результат разом із зібраним текстом відповіді.
|
|
22
|
+
- **backupWorkspacePackageFiles** — створює тимчасові бекапи `package.json` у воркспейсах для подальшого порівняння змін.
|
|
23
|
+
- **cleanupBackups** — прибирає тимчасові бекапи `package.json` після завершення прогону.
|
|
24
|
+
- **findCargoManifests** — знаходить `Cargo.toml` поза `node_modules`, `.worktrees` і `target` для інформаційного підсумку.
|
|
25
|
+
- **formatReport** — збирає лаконічний Markdown-звіт про minor/patch, major-оновлення, Rust-крейти та загальний обсяг змін.
|
|
26
|
+
- **runTazeOrchestrator** — виконує повний прогін taze: перевіряє worktree з `main.json`, робить бекап, оновлює залежності з `package.json`, обробляє major-оновлення по одному пакету, прибирає бекапи й повертає підсумок.
|
|
27
|
+
|
|
28
|
+
## Публічний API
|
|
29
|
+
|
|
30
|
+
- buildDependencyPrompt — Готує промпт для одного LLM-кроку taze: тільки breaking changes, сумісність і рефакторинг для одного major-пакета. Перші кроки аналізу та фінальну збірку звіту робить оркестратор без LLM.
|
|
31
|
+
- callRunner — Запускає один ітеративний виклик через вибраний раннер. Для `pi` бере текст із stdout вбудованого pi-агента, для `cursor` і `codex` отримує його напряму через ACP-міст.
|
|
32
|
+
- backupWorkspacePackageFiles — Зберігає копії `package.json` усіх workspace-пакетів перед змінами, щоб потім відрізнити major і minor оновлення.
|
|
33
|
+
- cleanupBackups — Видаляє тимчасові копії `package.json` після завершення роботи.
|
|
34
|
+
- findCargoManifests — Находить `Cargo.toml` у репозиторії поза службовими директоріями; використовується лише для огляду Rust-крейтів.
|
|
35
|
+
- formatReport — Складає фінальний звіт із результатів усіх ітерацій без окремого LLM-запиту.
|
|
36
|
+
- runTazeOrchestrator — Керує taze від початку до кінця: робить бекап, масово оновлює версії, збирає diff, прибирає тимчасові файли й формує звіт. Для кожного major-пакета окремо запускає обмежений LLM-виклик, щоб збій одного пакета не зупиняв інші.
|
|
37
|
+
|
|
38
|
+
Конфіги: `package.json`, `main.json`
|
|
39
|
+
|
|
40
|
+
## Гарантії поведінки
|
|
41
|
+
|
|
42
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
43
|
+
- Кешує результати в межах одного прогону.
|
|
44
|
+
- Свідомо пропускає шляхи: `node_modules`.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** @see ./docs/orchestrate.md */
|
|
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
|
+
|
|
7
|
+
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
8
|
+
import { collectTazeDiff } from './diff.mjs'
|
|
9
|
+
|
|
10
|
+
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
11
|
+
const BACKUP_SUFFIX = '.taze-bak'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Промпт ОДНОГО ітеративного виклику — лише кроки 4-6 SKILL.md (breaking
|
|
15
|
+
* changes → сумісність коду → рефакторинг) для ОДНОГО major-пакета. Кроки
|
|
16
|
+
* 1-3/7/8 виконує оркестратор детерміновано, без LLM.
|
|
17
|
+
* @param {{workspace: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectTazeDiff`)
|
|
18
|
+
* @returns {string} готовий промпт
|
|
19
|
+
*/
|
|
20
|
+
export function buildDependencyPrompt({ workspace, pkg, from, to }) {
|
|
21
|
+
return [
|
|
22
|
+
'# Major-оновлення одного пакета: перевірка сумісності й рефакторинг',
|
|
23
|
+
'',
|
|
24
|
+
`Пакет \`${pkg}\` у воркспейсі \`${workspace}\`: **${from} → ${to}** — вже застосовано в package.json/bun.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
25
|
+
'',
|
|
26
|
+
'## Кроки',
|
|
27
|
+
`1. Зібрати breaking changes цього оновлення: CHANGELOG/Releases репозиторію модуля (поле \`repository\` у \`node_modules/${pkg}/package.json\`), або git/diff між закешованою старою версією (\`~/.bun/install/cache/${pkg}@<стара-версія>/\`) і новою (\`node_modules/${pkg}/\`).`,
|
|
28
|
+
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n\` по імпортах/викликах \`${pkg}\`).`,
|
|
29
|
+
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом).',
|
|
30
|
+
'4. Якщо були правки — запусти `npx @7n/rules lint`, typecheck/test якщо є в проєкті.',
|
|
31
|
+
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
32
|
+
'',
|
|
33
|
+
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
34
|
+
].join('\n')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Диспетчер одного ітеративного виклику на обраний раннер. `pi` — вбудований
|
|
39
|
+
* pi-агент (`@7n/llm-lib/agent-skill`; текст перехоплюється через `deps.out`,
|
|
40
|
+
* бо `runAgentSkill` не повертає його напряму — лише стрімить у stdout).
|
|
41
|
+
* `cursor`/`codex` — napi-міст ACP (`@7n/llm-lib/acp`; текст — прямий return,
|
|
42
|
+
* idle-timeout і видимість прогресу вже вбудовані в сам міст).
|
|
43
|
+
* @param {'pi' | 'cursor' | 'codex'} runner раннер
|
|
44
|
+
* @param {string} prompt промпт для одного пакета
|
|
45
|
+
* @param {string} cwd робочий каталог
|
|
46
|
+
* @param {{ runAgentSkill?: (prompt: string, opts?: object) => Promise<{ok: boolean, error: string|null}>, runAcpAgent?: (kind: string, prompt: string, cwd: string) => Promise<string> }} [deps] інжекти для тестів
|
|
47
|
+
* @returns {Promise<{ ok: boolean, text: string, error: string|null }>} результат виклику
|
|
48
|
+
*/
|
|
49
|
+
export async function callRunner(runner, prompt, cwd, deps = {}) {
|
|
50
|
+
if (runner === 'pi') {
|
|
51
|
+
let runAgentSkill = deps.runAgentSkill
|
|
52
|
+
if (!runAgentSkill) {
|
|
53
|
+
const agentSkillModule = await import('@7n/llm-lib/agent-skill')
|
|
54
|
+
runAgentSkill = agentSkillModule.runAgentSkill
|
|
55
|
+
}
|
|
56
|
+
let text = ''
|
|
57
|
+
const result = await runAgentSkill(prompt, {
|
|
58
|
+
skillId: 'taze',
|
|
59
|
+
tier: 'avg',
|
|
60
|
+
cwd,
|
|
61
|
+
deps: { out: chunk => (text += chunk) }
|
|
62
|
+
})
|
|
63
|
+
return { ok: result.ok, text, error: result.error }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let runAcpAgent = deps.runAcpAgent
|
|
67
|
+
if (!runAcpAgent) {
|
|
68
|
+
const acpModule = await import('@7n/llm-lib/acp')
|
|
69
|
+
runAcpAgent = acpModule.runAcpAgent
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const text = await runAcpAgent(runner, prompt, cwd)
|
|
73
|
+
return { ok: true, text, error: null }
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { ok: false, text: '', error: error instanceof Error ? error.message : String(error) }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Перевіряє, що `cwd` — ізольований worktree (`main.json.worktree: true`,
|
|
81
|
+
* той самий контракт, що й для інших worktree-only скілів). Раніше цю
|
|
82
|
+
* гарантію тримав агент, читаючи SKILL.md-preflight як частину промпту;
|
|
83
|
+
* оркестратор більше НЕ годує SKILL.md жодному викликові, тож без цієї
|
|
84
|
+
* перевірки `bunx taze -w -r latest`/`bun install` мовчки виконались би
|
|
85
|
+
* прямо в основному дереві виклику. Кидає, якщо `git rev-parse --show-toplevel`
|
|
86
|
+
* не містить `.worktrees` як сегмент шляху (покриває і `npx \@7n/mt worktree
|
|
87
|
+
* create`-конвенцію `.worktrees/`, і сесійну `.claude/worktrees/`).
|
|
88
|
+
* @param {string} cwd каталог для перевірки
|
|
89
|
+
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
function assertRunningInWorktree(cwd, spawnFn) {
|
|
93
|
+
const result = spawnFn('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
|
|
94
|
+
const toplevel = result.status === 0 ? result.stdout.trim() : ''
|
|
95
|
+
const segments = new Set(toplevel.replaceAll('\\', '/').split('/'))
|
|
96
|
+
if (!segments.has('.worktrees')) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`taze: "${cwd}" не в ізольованому worktree (git toplevel: "${toplevel || '?'}"). ` +
|
|
99
|
+
'main.json.worktree=true вимагає окремого дерева — створи його спершу (див. SKILL.md preflight), не запускай taze в основному дереві.'
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Синхронно виконує детерміновану команду (bunx/bun/find), кидає з
|
|
106
|
+
* exit-кодом+stderr при провалі.
|
|
107
|
+
* @param {string} cmd бінарник
|
|
108
|
+
* @param {string[]} args аргументи
|
|
109
|
+
* @param {string} cwd робочий каталог
|
|
110
|
+
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
111
|
+
* @returns {string} stdout
|
|
112
|
+
*/
|
|
113
|
+
function runCommand(cmd, args, cwd, spawnFn) {
|
|
114
|
+
const result = spawnFn(cmd, args, { cwd, encoding: 'utf8' })
|
|
115
|
+
if (result.status !== 0) {
|
|
116
|
+
throw new Error(`${cmd} ${args.join(' ')} → exit ${result.status}: ${result.stderr || result.stdout}`)
|
|
117
|
+
}
|
|
118
|
+
return result.stdout
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Бекапить package.json кожного воркспейсу (крок 1 SKILL.md) — потрібно для
|
|
123
|
+
* класифікації major/minor через `collectTazeDiff` після bump-у.
|
|
124
|
+
* @param {string} cwd корінь репо
|
|
125
|
+
* @param {{ getMonorepoPackageRootDirs?: (cwd: string) => Promise<string[]>, copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжекти
|
|
126
|
+
* @returns {Promise<string[]>} відносні шляхи воркспейсів, що мали package.json
|
|
127
|
+
*/
|
|
128
|
+
export async function backupWorkspacePackageFiles(cwd, deps = {}) {
|
|
129
|
+
const getRoots = deps.getMonorepoPackageRootDirs ?? getMonorepoPackageRootDirs
|
|
130
|
+
const copy = deps.copyFile ?? copyFile
|
|
131
|
+
const roots = await getRoots(cwd)
|
|
132
|
+
const backedUp = []
|
|
133
|
+
for (const ws of roots) {
|
|
134
|
+
const pkgPath = join(cwd, ws, 'package.json')
|
|
135
|
+
if (!existsSync(pkgPath)) continue
|
|
136
|
+
await copy(pkgPath, `${pkgPath}${BACKUP_SUFFIX}`)
|
|
137
|
+
backedUp.push(ws)
|
|
138
|
+
}
|
|
139
|
+
return backedUp
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Прибирає бекапи package.json після завершення (крок 7 SKILL.md).
|
|
144
|
+
* @param {string} cwd корінь репо
|
|
145
|
+
* @param {string[]} workspaces воркспейси з бекапом (з `backupWorkspacePackageFiles`)
|
|
146
|
+
* @param {{ rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжект
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
export async function cleanupBackups(cwd, workspaces, deps = {}) {
|
|
150
|
+
const remove = deps.rm ?? rm
|
|
151
|
+
for (const ws of workspaces) {
|
|
152
|
+
await remove(join(cwd, ws, `package.json${BACKUP_SUFFIX}`), { force: true })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Знаходить Cargo.toml поза node_modules/.worktrees/target (крок 0.2
|
|
158
|
+
* SKILL.md). Лише інформаційно — v1 оркестратора Rust-крейти не оновлює
|
|
159
|
+
* (немає детермінованого cargo-diff-еквівалента `collectTazeDiff`,
|
|
160
|
+
* класифікація major там ручна за SKILL.md).
|
|
161
|
+
* @param {string} cwd корінь репо
|
|
162
|
+
* @param {{ spawnFn?: typeof spawnSync }} [deps] інжект
|
|
163
|
+
* @returns {string[]} відносні шляхи знайдених Cargo.toml
|
|
164
|
+
*/
|
|
165
|
+
export function findCargoManifests(cwd, deps = {}) {
|
|
166
|
+
const spawnFn = deps.spawnFn ?? spawnSync
|
|
167
|
+
const result = spawnFn(
|
|
168
|
+
'find',
|
|
169
|
+
[
|
|
170
|
+
'.',
|
|
171
|
+
'-name',
|
|
172
|
+
'Cargo.toml',
|
|
173
|
+
'-not',
|
|
174
|
+
'-path',
|
|
175
|
+
'*/node_modules/*',
|
|
176
|
+
'-not',
|
|
177
|
+
'-path',
|
|
178
|
+
'*/.worktrees/*',
|
|
179
|
+
'-not',
|
|
180
|
+
'-path',
|
|
181
|
+
'*/target/*'
|
|
182
|
+
],
|
|
183
|
+
{ cwd, encoding: 'utf8' }
|
|
184
|
+
)
|
|
185
|
+
return (result.stdout ?? '')
|
|
186
|
+
.split('\n')
|
|
187
|
+
.map(line => line.trim())
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Компонує підсумковий звіт (крок 8 SKILL.md) детерміновано з результатів
|
|
193
|
+
* ітерацій — без окремого LLM-виклику для самого звіту.
|
|
194
|
+
* @param {{ minorPatch: number, totalChanged: number, results: Array<{pkg:string, workspace:string, from:string, to:string, ok:boolean, error:string|null}>, rustCrates: string[] }} args дані звіту
|
|
195
|
+
* @returns {string} markdown-звіт
|
|
196
|
+
*/
|
|
197
|
+
export function formatReport({ minorPatch, totalChanged, results, rustCrates }) {
|
|
198
|
+
const lines = [
|
|
199
|
+
'## taze: підсумок',
|
|
200
|
+
'',
|
|
201
|
+
`- **Оновлено (minor/patch):** ${minorPatch}`,
|
|
202
|
+
`- **Major-оновлення:** ${results.length}`
|
|
203
|
+
]
|
|
204
|
+
for (const r of results) {
|
|
205
|
+
const status = r.ok ? '✅' : '❌'
|
|
206
|
+
const errorSuffix = r.error ? ` — ${r.error}` : ''
|
|
207
|
+
lines.push(` ${status} \`${r.pkg}\` (${r.workspace}): ${r.from} → ${r.to}${errorSuffix}`)
|
|
208
|
+
}
|
|
209
|
+
if (rustCrates.length > 0) {
|
|
210
|
+
lines.push(
|
|
211
|
+
'',
|
|
212
|
+
`- **Rust-крейти (${rustCrates.length}), потребують ручного прогону Rust-гілки SKILL.md:** ${rustCrates.join(', ')}`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
lines.push('', `- **Всього змінено:** ${totalChanged}`)
|
|
216
|
+
return lines.join('\n')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Оркеструє taze: детерміновані кроки (бекап → масовий bump → diff →
|
|
221
|
+
* прибирання → звіт) без LLM, і по одному ізольованому, обмеженому по
|
|
222
|
+
* обсягу виклику `callRunner` на кожен major-пакет (кроки 4-6 SKILL.md) —
|
|
223
|
+
* замість одного величезного непрозорого ходу на весь монорепо. Кожен
|
|
224
|
+
* виклик успадковує власний timeout/idle-timeout раннера, тож падіння
|
|
225
|
+
* одного пакета не втрачає прогрес по інших.
|
|
226
|
+
* @param {{
|
|
227
|
+
* cwd?: string,
|
|
228
|
+
* runner?: 'pi' | 'cursor' | 'codex',
|
|
229
|
+
* log?: (line: string) => void,
|
|
230
|
+
* deps?: { spawnFn?: typeof spawnSync, collectTazeDiff?: (cwd: string) => Promise<object>, callRunner?: (runner: string, prompt: string, cwd: string, deps: object) => Promise<{ok: boolean, text: string, error: string|null}> } & Record<string, unknown>
|
|
231
|
+
* }} [options] опції + інжекти для тестів
|
|
232
|
+
* @returns {Promise<{ ok: boolean, report: string, results: Array<object> }>} результат
|
|
233
|
+
*/
|
|
234
|
+
export async function runTazeOrchestrator(options = {}) {
|
|
235
|
+
const cwd = options.cwd ?? process.cwd()
|
|
236
|
+
const runner = options.runner ?? 'pi'
|
|
237
|
+
const log = options.log ?? (line => console.log(line))
|
|
238
|
+
const deps = options.deps ?? {}
|
|
239
|
+
const spawnFn = deps.spawnFn ?? spawnSync
|
|
240
|
+
|
|
241
|
+
assertRunningInWorktree(cwd, spawnFn)
|
|
242
|
+
|
|
243
|
+
const rustCrates = findCargoManifests(cwd, { spawnFn })
|
|
244
|
+
|
|
245
|
+
log('📦 Бекап package.json...')
|
|
246
|
+
const backedUpWorkspaces = await backupWorkspacePackageFiles(cwd, deps)
|
|
247
|
+
|
|
248
|
+
log('⬆️ bunx taze -w -r latest...')
|
|
249
|
+
runCommand('bunx', ['taze', '-w', '-r', 'latest'], cwd, spawnFn)
|
|
250
|
+
log('📥 bun install...')
|
|
251
|
+
runCommand('bun', ['install'], cwd, spawnFn)
|
|
252
|
+
|
|
253
|
+
const collectDiff = deps.collectTazeDiff ?? collectTazeDiff
|
|
254
|
+
const diff = await collectDiff(cwd)
|
|
255
|
+
log(`🔍 diff: ${diff.major.length} major, ${diff.minorPatch} minor/patch`)
|
|
256
|
+
|
|
257
|
+
const results = []
|
|
258
|
+
const call = deps.callRunner ?? callRunner
|
|
259
|
+
for (const entry of diff.major) {
|
|
260
|
+
log(`🔧 ${entry.pkg} (${entry.workspace}): ${entry.from} → ${entry.to}...`)
|
|
261
|
+
const outcome = await call(runner, buildDependencyPrompt(entry), cwd, deps)
|
|
262
|
+
results.push({ ...entry, ...outcome })
|
|
263
|
+
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await cleanupBackups(cwd, backedUpWorkspaces, deps)
|
|
267
|
+
|
|
268
|
+
const report = formatReport({ minorPatch: diff.minorPatch, totalChanged: diff.totalChanged, results, rustCrates })
|
|
269
|
+
log(report)
|
|
270
|
+
|
|
271
|
+
return { ok: results.every(r => r.ok), report, results }
|
|
272
|
+
}
|