@7n/rules 1.14.1 → 1.15.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 +2 -1
- package/scripts/lib/docs/index.md +4 -1
- package/scripts/lib/docs/plugin-api.md +31 -0
- package/scripts/lib/docs/resolve-plugins.md +4 -4
- package/scripts/lib/plugin-api.mjs +71 -0
- package/scripts/lib/resolve-plugins.mjs +31 -6
- package/skills/taze/SKILL.md +8 -1
- package/skills/taze/js/docs/index.md +6 -6
- package/skills/taze/js/docs/orchestrate.md +12 -17
- package/skills/taze/js/docs/rust-provider.md +42 -0
- package/skills/taze/js/orchestrate.mjs +113 -334
- package/skills/taze/js/rust-provider.mjs +162 -0
- package/skills/taze/js/docs/uv-diff.md +0 -41
- package/skills/taze/js/uv-diff.mjs +0 -176
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** @see ./docs/rust-provider.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 { collectCargoDiff } from './cargo-diff.mjs'
|
|
8
|
+
|
|
9
|
+
/** Суфікс бекапу — той самий, що й для package.json (крок 1 SKILL.md, Rust-гілка). */
|
|
10
|
+
const BACKUP_SUFFIX = '.taze-bak'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Промпт ОДНОГО ітеративного виклику для Rust-крейта (кроки 4-6 SKILL.md,
|
|
14
|
+
* Rust-гілка) для ОДНОГО major-крейта. Кроки 1-3/7/8 виконує оркестратор
|
|
15
|
+
* детерміновано, без LLM.
|
|
16
|
+
* @param {{manifest: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectCargoDiff`)
|
|
17
|
+
* @returns {string} готовий промпт
|
|
18
|
+
*/
|
|
19
|
+
export function buildCargoDependencyPrompt({ manifest, pkg, from, to }) {
|
|
20
|
+
return [
|
|
21
|
+
'# Major-оновлення одного Rust-крейта: перевірка сумісності й рефакторинг',
|
|
22
|
+
'',
|
|
23
|
+
`Крейт \`${pkg}\` у \`${manifest}\`: **${from} → ${to}** — вже застосовано в Cargo.toml/Cargo.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
24
|
+
'',
|
|
25
|
+
'## Кроки',
|
|
26
|
+
`1. Зібрати breaking changes цього оновлення: адреса репозиторію з поля \`repository\`/\`documentation\` крейта на crates.io (https://crates.io/crates/${pkg}) — CHANGELOG.md репозиторію чи GitHub Releases. Якщо немає — різниця по публічному API (\`pub fn\`/\`pub struct\`/\`pub trait\`) між закешованою старою версією (\`~/.cargo/registry/src/*/${pkg}-<стара-версія>/\`) і новою.`,
|
|
27
|
+
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n --type rust\` по use-шляхах/викликах \`${pkg}\`).`,
|
|
28
|
+
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати use-шлях, оновити сигнатуру виклику, замінити видалений макрос еквівалентом).',
|
|
29
|
+
'4. Якщо були правки — запусти `cargo fmt --all -- --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test`.',
|
|
30
|
+
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
31
|
+
'',
|
|
32
|
+
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
33
|
+
].join('\n')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Знаходить Cargo.toml поза node_modules/.worktrees/target (крок 0.2 SKILL.md).
|
|
38
|
+
* @param {string} cwd корінь репо
|
|
39
|
+
* @param {{ spawnFn?: (cmd: string, args: string[], opts?: object) => { status: number|null, stdout: string, stderr: string } }} deps інжект зі spawnSync-сумісним викликом
|
|
40
|
+
* @returns {string[]} відносні шляхи знайдених Cargo.toml
|
|
41
|
+
*/
|
|
42
|
+
export function findCargoManifests(cwd, deps = {}) {
|
|
43
|
+
const spawnFn = deps.spawnFn ?? spawnSync
|
|
44
|
+
const result = spawnFn(
|
|
45
|
+
'find',
|
|
46
|
+
[
|
|
47
|
+
'.',
|
|
48
|
+
'-name',
|
|
49
|
+
'Cargo.toml',
|
|
50
|
+
'-not',
|
|
51
|
+
'-path',
|
|
52
|
+
'*/node_modules/*',
|
|
53
|
+
'-not',
|
|
54
|
+
'-path',
|
|
55
|
+
'*/.worktrees/*',
|
|
56
|
+
'-not',
|
|
57
|
+
'-path',
|
|
58
|
+
'*/target/*'
|
|
59
|
+
],
|
|
60
|
+
{ cwd, encoding: 'utf8' }
|
|
61
|
+
)
|
|
62
|
+
return (result.stdout ?? '')
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map(line => line.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Чи встановлений cargo-edit (дає `cargo upgrade`) — без нього неможливо
|
|
70
|
+
* детерміновано перетнути major-межу Rust-залежностей (голий `cargo update`
|
|
71
|
+
* піднімає лише semver-сумісні версії; SKILL.md §Передумови).
|
|
72
|
+
* @param {import('../../../scripts/lib/plugin-api.mjs').SpawnFn} spawnFn spawnSync-сумісний виклик
|
|
73
|
+
* @returns {boolean} true — `cargo upgrade` доступна
|
|
74
|
+
*/
|
|
75
|
+
function hasCargoEdit(spawnFn) {
|
|
76
|
+
return spawnFn('cargo', ['upgrade', '--version'], { encoding: 'utf8' }).status === 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Бекапить кожен Cargo.toml + спільний кореневий Cargo.lock (крок 1 SKILL.md,
|
|
81
|
+
* Rust-гілка). v1: один спільний workspace/Cargo.lock у корені `cwd` —
|
|
82
|
+
* поточна реальна топологія репо; кілька незалежних Cargo-workspace поки не
|
|
83
|
+
* підтримується.
|
|
84
|
+
* @param {string} cwd корінь репо
|
|
85
|
+
* @param {string[]} manifestPaths відносні шляхи Cargo.toml (з `findCargoManifests`)
|
|
86
|
+
* @param {{ copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжект
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
export async function backupCargoManifests(cwd, manifestPaths, deps = {}) {
|
|
90
|
+
const copy = deps.copyFile ?? copyFile
|
|
91
|
+
for (const manifest of manifestPaths) {
|
|
92
|
+
const manifestPath = join(cwd, manifest)
|
|
93
|
+
if (existsSync(manifestPath)) await copy(manifestPath, `${manifestPath}${BACKUP_SUFFIX}`)
|
|
94
|
+
}
|
|
95
|
+
const lockPath = join(cwd, 'Cargo.lock')
|
|
96
|
+
if (existsSync(lockPath)) await copy(lockPath, `${lockPath}${BACKUP_SUFFIX}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Прибирає бекапи Cargo.toml/Cargo.lock після завершення (крок 7 SKILL.md,
|
|
101
|
+
* Rust-гілка).
|
|
102
|
+
* @param {string} cwd корінь репо
|
|
103
|
+
* @param {string[]} manifestPaths відносні шляхи Cargo.toml (з `findCargoManifests`)
|
|
104
|
+
* @param {{ rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжект
|
|
105
|
+
* @returns {Promise<void>}
|
|
106
|
+
*/
|
|
107
|
+
export async function cleanupCargoBackups(cwd, manifestPaths, deps = {}) {
|
|
108
|
+
const remove = deps.rm ?? rm
|
|
109
|
+
for (const manifest of manifestPaths) {
|
|
110
|
+
await remove(join(cwd, `${manifest}${BACKUP_SUFFIX}`), { force: true })
|
|
111
|
+
}
|
|
112
|
+
await remove(join(cwd, `Cargo.lock${BACKUP_SUFFIX}`), { force: true })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Виконує cargo-команду, кидає з exit-кодом+stderr при провалі.
|
|
117
|
+
* @param {string[]} args аргументи cargo
|
|
118
|
+
* @param {string} cwd робочий каталог
|
|
119
|
+
* @param {import('../../../scripts/lib/plugin-api.mjs').SpawnFn} spawnFn spawnSync-сумісний виклик
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
function runCargo(args, cwd, spawnFn) {
|
|
123
|
+
const result = spawnFn('cargo', args, { cwd, encoding: 'utf8' })
|
|
124
|
+
if (result.status !== 0) {
|
|
125
|
+
throw new Error(`cargo ${args.join(' ')} → exit ${result.status}: ${result.stderr || result.stdout}`)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Вбудований (first-party) EcosystemProvider Rust/Cargo для taze-оркестратора —
|
|
131
|
+
* форма контракту `@7n/rules/plugin-api`. Лишається в ядрі до фази 2
|
|
132
|
+
* (spec 2026-07-18-lang-plugins-extraction), далі виїде в `@7n/rules-lang-rust`.
|
|
133
|
+
* @type {import('../../../scripts/lib/plugin-api.mjs').EcosystemProvider}
|
|
134
|
+
*/
|
|
135
|
+
export const rustProvider = {
|
|
136
|
+
id: 'rust-cargo',
|
|
137
|
+
title: 'Rust-крейти',
|
|
138
|
+
manifestNoun: 'Cargo.toml',
|
|
139
|
+
skillSection: 'Rust-гілкою SKILL.md',
|
|
140
|
+
detect: (cwd, deps) => findCargoManifests(cwd, deps),
|
|
141
|
+
available: spawnFn =>
|
|
142
|
+
hasCargoEdit(spawnFn)
|
|
143
|
+
? { ok: true, reason: null }
|
|
144
|
+
: {
|
|
145
|
+
ok: false,
|
|
146
|
+
reason:
|
|
147
|
+
'cargo-edit не встановлено (`cargo install cargo-edit`) — cargo update без нього недетермінований для major'
|
|
148
|
+
},
|
|
149
|
+
backup: backupCargoManifests,
|
|
150
|
+
bump: (cwd, manifests, { spawnFn, log }) => {
|
|
151
|
+
log('⬆️ cargo upgrade --incompatible allow...')
|
|
152
|
+
runCargo(['upgrade', '--incompatible', 'allow'], cwd, spawnFn)
|
|
153
|
+
log('🔄 cargo update...')
|
|
154
|
+
runCargo(['update'], cwd, spawnFn)
|
|
155
|
+
return Promise.resolve()
|
|
156
|
+
},
|
|
157
|
+
diff: (cwd, manifests) => collectCargoDiff(cwd, manifests),
|
|
158
|
+
promptFor: buildCargoDependencyPrompt,
|
|
159
|
+
cleanup: cleanupCargoBackups
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default rustProvider
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: uv-diff.mjs
|
|
4
|
-
resource: npm/skills/taze/js/uv-diff.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: a23e68cc
|
|
7
|
-
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.99
|
|
10
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Огляд
|
|
14
|
-
|
|
15
|
-
Працює з залежностями `pyproject.toml` у форматах PEP 508 і PEP 440: `parsePep508` і `parsePep440Version` розбирають записи, `extractLowerBoundVersion` дістає нижню межу версії, `diffPyprojectDeps` і `collectUvDiff` порівнюють прямі залежності та формують diff відносно backup-версії, а `listDirectDependencies` повертає список прямих залежностей. Код read-only (лише читає файли з диска), fail-safe перехоплює помилки без винятків назовні та для частини збоїв повертає порожнє значення замість помилки.
|
|
16
|
-
|
|
17
|
-
## Поведінка
|
|
18
|
-
|
|
19
|
-
- `parsePep508` — розбирає рядок залежності у форматі PEP 508 на назву пакета, extras і version specifier; для невалідного рядка повертає порожній результат.
|
|
20
|
-
- `parsePep440Version` — витягує ядро версії PEP 440 і зводить відсутні компоненти до нулів; для не-версійного рядка повертає порожній результат.
|
|
21
|
-
- `extractLowerBoundVersion` — дістає нижню межу версійного specifier-а, тобто версію, яку відповідає нижньому bound у залежності; якщо межі немає, повертає порожній результат.
|
|
22
|
-
- `diffPyprojectDeps` — порівнює прямі залежності двох `pyproject.toml` за іменем пакета і класифікує зміну як major або minor/patch.
|
|
23
|
-
- `collectUvDiff` — збирає diff для `pyproject.toml` між поточним файлом і його backup-версією, а за відсутності потрібних файлів повертає порожній diff.
|
|
24
|
-
- `listDirectDependencies` — повертає список прямих залежностей із `pyproject.toml` у вигляді назв пакетів та extras.
|
|
25
|
-
|
|
26
|
-
## Публічний API
|
|
27
|
-
|
|
28
|
-
- parsePep508 — Розбирає один PEP 508-запис залежності на назву пакета, extras і version specifier.
|
|
29
|
-
- parsePep440Version — Виділяє основні компоненти PEP 440-версії; відсутні частини вважає нулями.
|
|
30
|
-
- extractLowerBoundVersion — Витягає найнижчу допустиму версію з набору `>=` / `==` / `~=`-обмежень.
|
|
31
|
-
- diffPyprojectDeps — Порівнює списки `[project].dependencies` у двох `pyproject.toml` за назвою пакета.
|
|
32
|
-
- collectUvDiff — Збирає різницю між поточним `pyproject.toml` і його backup-копією в `cwd`.
|
|
33
|
-
- listDirectDependencies — Повертає прямі залежності з `[project].dependencies` поточного `pyproject.toml` разом з extras.
|
|
34
|
-
|
|
35
|
-
Конфіги, на які спирається код: package.json
|
|
36
|
-
|
|
37
|
-
## Гарантії поведінки
|
|
38
|
-
|
|
39
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
40
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
41
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/uv-diff.md */
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { parse as parseToml } from 'smol-toml'
|
|
6
|
-
|
|
7
|
-
import { isBreaking } from './diff.mjs'
|
|
8
|
-
|
|
9
|
-
/** Дефолтний суфікс бекапу — той самий, що й для package.json/Cargo.toml. */
|
|
10
|
-
const DEFAULT_BACKUP_SUFFIX = '.taze-bak'
|
|
11
|
-
|
|
12
|
-
// Ім'я пакета (PEP 508: літери/цифри/`._-`) + опційні `[extras]` + решта
|
|
13
|
-
// (версійний specifier, можливо з `; marker` — ігнорується, не наш скоуп).
|
|
14
|
-
const PEP508_RE = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(\[[^\]]*])?\s*([^;]*)/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Парсить один PEP 508-рядок залежності (`"typer>=0.19.1,<0.20.0"`,
|
|
18
|
-
* `"strawberry-graphql[asgi]>=0.282.0"`) на ім'я/extras/specifier.
|
|
19
|
-
* @param {string} requirement рядок із `[project].dependencies`
|
|
20
|
-
* @returns {{name: string, extras: string[], specifier: string}|null} розбір, або null для невалідного рядка
|
|
21
|
-
*/
|
|
22
|
-
export function parsePep508(requirement) {
|
|
23
|
-
if (typeof requirement !== 'string') return null
|
|
24
|
-
const m = PEP508_RE.exec(requirement)
|
|
25
|
-
if (!m) return null
|
|
26
|
-
const extras = m[2]
|
|
27
|
-
? m[2]
|
|
28
|
-
.slice(1, -1)
|
|
29
|
-
.split(',')
|
|
30
|
-
.map(s => s.trim())
|
|
31
|
-
.filter(Boolean)
|
|
32
|
-
: []
|
|
33
|
-
return { name: m[1], extras, specifier: (m[3] ?? '').trim() }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 1-3 числові компоненти PEP 440 (`"1"`, `"0.4"`, `"1.2.3"`), без урахування
|
|
37
|
-
// pre/post/dev-суфіксів (поза скоупом major/minor/patch-класифікації).
|
|
38
|
-
const PEP440_VERSION_RE = /^[\s=><~!]*(\d+)(?:\.(\d+))?(?:\.(\d+))?/
|
|
39
|
-
// Операторний префікс версійного сегмента (`>=`, `==`, `~=`, `!=` тощо) — для відсікання при витягуванні голої версії.
|
|
40
|
-
const VERSION_OPERATOR_PREFIX_RE = /^[>=~!]+/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Парсить ядро PEP 440-версії (major.minor.patch, відсутні компоненти → 0).
|
|
44
|
-
* @param {string} spec версійний рядок (сегмент specifier-а, без операторного префікса)
|
|
45
|
-
* @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-версії
|
|
46
|
-
*/
|
|
47
|
-
export function parsePep440Version(spec) {
|
|
48
|
-
if (typeof spec !== 'string') return null
|
|
49
|
-
const m = PEP440_VERSION_RE.exec(spec)
|
|
50
|
-
if (!m) return null
|
|
51
|
-
return { major: Number(m[1]), minor: Number(m[2] ?? 0), patch: Number(m[3] ?? 0) }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Дістає нижню межу версійного specifier-а (перший сегмент `>=`/`==`/`~=`
|
|
56
|
-
* серед comma-separated списку, напр. `">=0.19.1,<0.20.0"` → `"0.19.1"`).
|
|
57
|
-
* Саме нижня межа відповідає версії, яку реально ставить `uv add --bounds lower`.
|
|
58
|
-
* @param {string} specifier версійний specifier із `parsePep508`
|
|
59
|
-
* @returns {string|null} версія нижньої межі, або null якщо немає жодного `>=`/`==`/`~=`-сегмента
|
|
60
|
-
*/
|
|
61
|
-
export function extractLowerBoundVersion(specifier) {
|
|
62
|
-
if (!specifier) return null
|
|
63
|
-
const clause = specifier
|
|
64
|
-
.split(',')
|
|
65
|
-
.map(s => s.trim())
|
|
66
|
-
.find(c => c.startsWith('>=') || c.startsWith('==') || c.startsWith('~='))
|
|
67
|
-
return clause ? clause.replace(VERSION_OPERATOR_PREFIX_RE, '').trim() : null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Порівнює `[project].dependencies` двох pyproject.toml — той самий контракт,
|
|
72
|
-
* що й `diffPackageJson`/`diffCargoToml`. Матчинг по ІМЕНІ пакета (не по
|
|
73
|
-
* позиції в масиві — PEP 621 `dependencies` це список PEP 508-рядків, не
|
|
74
|
-
* мапа ім'я→версія).
|
|
75
|
-
* @param {object} oldManifest розпарсений старий pyproject.toml (бекап)
|
|
76
|
-
* @param {object} newManifest розпарсений новий pyproject.toml
|
|
77
|
-
* @param {string} manifest відносний шлях pyproject.toml (мітка джерела запису)
|
|
78
|
-
* @returns {{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number}} зміни
|
|
79
|
-
*/
|
|
80
|
-
export function diffPyprojectDeps(oldManifest, newManifest, manifest) {
|
|
81
|
-
const oldDeps = oldManifest?.project?.dependencies ?? []
|
|
82
|
-
const newByName = new Map()
|
|
83
|
-
for (const requirement of newDeps(newManifest)) {
|
|
84
|
-
const parsed = parsePep508(requirement)
|
|
85
|
-
if (parsed) newByName.set(parsed.name, parsed)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const major = []
|
|
89
|
-
let minorPatch = 0
|
|
90
|
-
for (const oldRequirement of oldDeps) {
|
|
91
|
-
const oldParsed = parsePep508(oldRequirement)
|
|
92
|
-
if (!oldParsed) continue
|
|
93
|
-
const newParsed = newByName.get(oldParsed.name)
|
|
94
|
-
if (!newParsed || newParsed.specifier === oldParsed.specifier) continue
|
|
95
|
-
|
|
96
|
-
const from = extractLowerBoundVersion(oldParsed.specifier)
|
|
97
|
-
const to = extractLowerBoundVersion(newParsed.specifier)
|
|
98
|
-
if (from === null || to === null || from === to) continue
|
|
99
|
-
|
|
100
|
-
const fromV = parsePep440Version(from)
|
|
101
|
-
const toV = parsePep440Version(to)
|
|
102
|
-
if (fromV && toV && isBreaking(fromV, toV)) {
|
|
103
|
-
major.push({ manifest, pkg: oldParsed.name, from, to })
|
|
104
|
-
} else {
|
|
105
|
-
minorPatch += 1
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return { major, minorPatch }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @param {object} manifest розпарсений pyproject.toml
|
|
113
|
-
* @returns {string[]} `[project].dependencies` (порожній масив, якщо відсутнє)
|
|
114
|
-
*/
|
|
115
|
-
function newDeps(manifest) {
|
|
116
|
-
return manifest?.project?.dependencies ?? []
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Читає й парсить TOML-файл, або повертає null, якщо файл відсутній/невалідний.
|
|
121
|
-
* @param {string} path абсолютний шлях
|
|
122
|
-
* @returns {Promise<object|null>} розпарсений обʼєкт або null
|
|
123
|
-
*/
|
|
124
|
-
async function readTomlOrNull(path) {
|
|
125
|
-
if (!existsSync(path)) return null
|
|
126
|
-
try {
|
|
127
|
-
return parseToml(await readFile(path, 'utf8'))
|
|
128
|
-
} catch {
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Збирає diff по pyproject.toml: порівнює `<cwd>/pyproject.toml` з
|
|
135
|
-
* `<cwd>/pyproject.toml<backupSuffix>` — той самий контракт, що й
|
|
136
|
-
* `collectTazeDiff`/`collectCargoDiff`.
|
|
137
|
-
* @param {string} cwd корінь репозиторію (де лежить pyproject.toml — uv-конвенція: один кореневий файл, не per-crate, як Cargo.toml)
|
|
138
|
-
* @param {string} [backupSuffix] суфікс бекап-файлу
|
|
139
|
-
* @returns {Promise<{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number, totalChanged:number, comparedManifests:number}>} агрегований diff
|
|
140
|
-
*/
|
|
141
|
-
export async function collectUvDiff(cwd, backupSuffix = DEFAULT_BACKUP_SUFFIX) {
|
|
142
|
-
const oldManifest = await readTomlOrNull(join(cwd, `pyproject.toml${backupSuffix}`))
|
|
143
|
-
const newManifest = await readTomlOrNull(join(cwd, 'pyproject.toml'))
|
|
144
|
-
if (!oldManifest || !newManifest) {
|
|
145
|
-
return { major: [], minorPatch: 0, totalChanged: 0, comparedManifests: 0 }
|
|
146
|
-
}
|
|
147
|
-
const res = diffPyprojectDeps(oldManifest, newManifest, 'pyproject.toml')
|
|
148
|
-
return {
|
|
149
|
-
major: res.major,
|
|
150
|
-
minorPatch: res.minorPatch,
|
|
151
|
-
totalChanged: res.major.length + res.minorPatch,
|
|
152
|
-
comparedManifests: 1
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Дістає `{name, extras, raw}` кожної прямої залежності з
|
|
158
|
-
* `[project].dependencies` поточного pyproject.toml — вхід для
|
|
159
|
-
* per-пакетного bump-циклу (`uv` немає єдиної команди "підняти все до
|
|
160
|
-
* latest, навіть через major", на відміну від `bunx taze -w -r latest`/
|
|
161
|
-
* `cargo upgrade --incompatible allow` — підтверджено емпірично: `uv add
|
|
162
|
-
* <pkg>` на вже присутній залежності НЕ переписує specifier, поки не буде
|
|
163
|
-
* `uv remove` спершу). `raw` — оригінальний PEP 508-рядок, потрібен для
|
|
164
|
-
* best-effort відновлення, якщо `uv add` після `uv remove` не вдався.
|
|
165
|
-
* @param {object} manifest розпарсений pyproject.toml
|
|
166
|
-
* @returns {Array<{name: string, extras: string[], raw: string}>} прямі залежності
|
|
167
|
-
*/
|
|
168
|
-
export function listDirectDependencies(manifest) {
|
|
169
|
-
return newDeps(manifest)
|
|
170
|
-
.map(requirement => {
|
|
171
|
-
const parsed = parsePep508(requirement)
|
|
172
|
-
return parsed ? { ...parsed, raw: requirement } : null
|
|
173
|
-
})
|
|
174
|
-
.filter(Boolean)
|
|
175
|
-
.map(({ name, extras, raw }) => ({ name, extras, raw }))
|
|
176
|
-
}
|