diff --git a/docs/fr/how-to/customize-bmad.md b/docs/fr/how-to/customize-bmad.md index 6cb2cc54d..45382b817 100644 --- a/docs/fr/how-to/customize-bmad.md +++ b/docs/fr/how-to/customize-bmad.md @@ -22,7 +22,7 @@ Le skill `bmad-customize` est un assistant de rédaction guidée pour les **opti :::note[Prérequis] - BMad installé dans votre projet (voir [Comment installer BMad](./install-bmad.md)) -- Python 3.11+ sur votre PATH (pour le script de résolution — utilise `tomllib` de la bibliothèque standard, pas de `pip install`, pas de `uv`, pas de virtualenv) +- Un moyen d’exécuter le script de résolution — BMad adopte `uv` comme standard (`uv run`, qui provisionne Python pour vous) ; un simple `python3` 3.11+ sur votre PATH fonctionne toujours pendant la transition. Le script n’utilise que `tomllib` de la bibliothèque standard, il n’y a donc rien à `pip install`. - Un éditeur de texte pour les fichiers TOML ::: @@ -201,15 +201,15 @@ persistent_facts = [ ## Comment fonctionne la résolution -À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe), donc `python3` suffit : +À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise uniquement le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe). BMad adopte `uv run` comme standard pour exécuter ces scripts (uv provisionne un Python adapté pour vous) ; un simple `python3` fonctionne toujours pendant la transition : ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Pas de `pip install`, pas de `uv`, pas de virtualenv. Vérifiez avec `python3 --version`. Certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément. +**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Rien à `pip install`. L’exécution via `uv run` est le standard à venir — uv résout un interpréteur adapté pour vous. Si vous l’exécutez directement avec `python3` pendant la transition, vérifiez votre version avec `python3 --version` ; certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément. `--skill` pointe vers le répertoire installé du skill (où se trouve `customize.toml`). Le nom du skill est déduit du basename du répertoire, et le script cherche automatiquement `_bmad/custom/{skill-name}.toml` et `{skill-name}.user.toml`. @@ -217,17 +217,17 @@ Exemples d’utilisation : ```bash # Résoudre le bloc agent complet -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm \ --key agent # Résoudre un seul champ -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm \ --key agent.icon # Dump complet -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /chemin/absolu/vers/bmad-agent-pm ``` diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md index 9433a8820..0458607a0 100644 --- a/docs/how-to/customize-bmad.md +++ b/docs/how-to/customize-bmad.md @@ -22,7 +22,7 @@ The `bmad-customize` skill is a guided authoring helper for the **per-skill agen :::note[Prerequisites] - BMad installed in your project (see [How to Install BMad](./install-bmad.md)) -- Python 3.11+ on your PATH (for the resolver script -- uses stdlib `tomllib`, no `pip install`, no `uv`, no virtualenv) +- A way to run the resolver script — BMad is standardizing on `uv` (`uv run`, which provisions Python for you); a plain `python3` 3.11+ on your PATH still works during the transition. The script uses only stdlib `tomllib`, so there's nothing to `pip install`. - A text editor for TOML files ::: @@ -201,15 +201,15 @@ persistent_facts = [ ## How Resolution Works -On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses the Python standard library's `tomllib` module (no external dependencies), so plain `python3` is enough: +On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses only the Python standard library's `tomllib` module (no external dependencies). BMad is standardizing on `uv run` to invoke these scripts (uv provisions a suitable Python for you); a plain `python3` still works during the transition: ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`). No `pip install`, no `uv`, no virtualenv. Check with `python3 --version`. Some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier, so you may need to install 3.11+ separately. +**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`); nothing to `pip install`. Running via `uv run` is the going-forward standard — uv resolves a suitable interpreter for you. If you run it with `python3` directly during the transition, check your version with `python3 --version`: some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier, so you may need to install 3.11+ separately. `--skill` points at the skill's installed directory (where `customize.toml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/custom/{skill-name}.toml` and `{skill-name}.user.toml` automatically. @@ -217,17 +217,17 @@ Useful invocations: ```bash # Resolve the full agent block -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm \ --key agent # Resolve a single field -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm \ --key agent.icon # Full dump -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /abs/path/to/bmad-agent-pm ``` diff --git a/docs/vi-vn/how-to/customize-bmad.md b/docs/vi-vn/how-to/customize-bmad.md index eecc14728..6ac065e17 100644 --- a/docs/vi-vn/how-to/customize-bmad.md +++ b/docs/vi-vn/how-to/customize-bmad.md @@ -22,7 +22,7 @@ Skill `bmad-customize` là trợ lý tạo cấu hình có hướng dẫn cho ** :::note[Điều kiện tiên quyết] - BMad đã được cài trong dự án của bạn (xem [Cách cài đặt BMad](./install-bmad.md)) -- Python 3.11+ có trên PATH của bạn (để chạy resolver; dùng stdlib `tomllib`, không cần `pip install`, `uv` hay virtualenv) +- Một cách để chạy resolver script — BMad đang chuẩn hóa sang `uv` (`uv run`, tự cấp Python cho bạn); một `python3` 3.11+ thuần trên PATH vẫn dùng được trong giai đoạn chuyển đổi. Script chỉ dùng stdlib `tomllib`, nên không cần `pip install` gì cả. - Một trình soạn thảo văn bản cho file TOML ::: @@ -201,15 +201,15 @@ persistent_facts = [ ## Cách quá trình resolve diễn ra -Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này dùng `tomllib` của Python stdlib, nên `python3` thuần là đủ: +Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này chỉ dùng `tomllib` của Python stdlib (không có dependency ngoài). BMad đang chuẩn hóa sang `uv run` để chạy các script này (uv tự cấp một bản Python phù hợp cho bạn); một `python3` thuần vẫn dùng được trong giai đoạn chuyển đổi: ```bash -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} \ --key agent ``` -**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`. Không cần `pip install`, không cần `uv`, không cần virtualenv. Bạn có thể kiểm tra bằng `python3 --version`. Trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng. +**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`; không cần `pip install` gì. Chạy qua `uv run` là chuẩn về sau — uv tự tìm một bản interpreter phù hợp cho bạn. Nếu bạn chạy trực tiếp bằng `python3` trong giai đoạn chuyển đổi, hãy kiểm tra phiên bản bằng `python3 --version`: trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng. `--skill` trỏ vào thư mục skill đã cài, nơi có file `customize.toml`. Tên skill được lấy từ basename của thư mục, sau đó script sẽ tự tìm `_bmad/custom/{skill-name}.toml` và `{skill-name}.user.toml`. @@ -217,17 +217,17 @@ Một số lệnh hữu ích: ```bash # Resolve toàn bộ block agent -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \ --key agent # Resolve một trường cụ thể -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \ --key agent.icon # Dump toàn bộ -python3 {project-root}/_bmad/scripts/resolve_customization.py \ +uv run {project-root}/_bmad/scripts/resolve_customization.py \ --skill /duong-dan/tuyet-doi/toi/bmad-agent-pm ``` diff --git a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py index 5da6955a6..951bd4e77 100644 --- a/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +++ b/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py @@ -207,11 +207,11 @@ def test_unknown_category_style_uses_fallback_glyph(): def test_shipped_selector_is_in_sync_with_catalog(): # foolproofing: if someone edits brain-methods.csv they must regenerate the page. - # Regenerate with: python3 brain.py html --out assets/brain-selector.html + # Regenerate with: uv run brain.py html --out assets/brain-selector.html asset = brain.DEFAULT_FILE.parent / "brain-selector.html" assert asset.is_file(), "missing assets/brain-selector.html — generate it" expected = brain.html_doc(brain.load(brain.DEFAULT_FILE)) assert asset.read_text(encoding="utf-8") == expected, ( "assets/brain-selector.html is stale; regenerate: " - "python3 brain.py html --out assets/brain-selector.html" + "uv run brain.py html --out assets/brain-selector.html" ) diff --git a/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py b/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py index a7be22ece..916b7c3b9 100644 --- a/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +++ b/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py @@ -13,7 +13,7 @@ Exercises the scanner against a synthesized install tree: - malformed TOML (surfaces as an error without aborting) - multiple skills roots (e.g. project-local + user-global mix) -Run: python3 scripts/tests/test_list_customizable_skills.py +Run: uv run scripts/tests/test_list_customizable_skills.py """ from __future__ import annotations diff --git a/src/scripts/resolve_config.py b/src/scripts/resolve_config.py index eb9e20288..fd762e778 100644 --- a/src/scripts/resolve_config.py +++ b/src/scripts/resolve_config.py @@ -10,12 +10,14 @@ Reads from four layers (highest priority last): Outputs merged JSON to stdout. Errors go to stderr. -Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`, -no virtualenv — plain `python3` is sufficient. +Uses only the Python stdlib (`tomllib`) — no third-party dependencies. +BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable +interpreter for you); a plain `python3` on PATH still works during the +transition. Either runner needs Python 3.11+ for `tomllib`. - python3 resolve_config.py --project-root /abs/path/to/project - python3 resolve_config.py --project-root ... --key core - python3 resolve_config.py --project-root ... --key agents + uv run resolve_config.py --project-root /abs/path/to/project + uv run resolve_config.py --project-root ... --key core + uv run resolve_config.py --project-root ... --key agents Merge rules (same as resolve_customization.py): - Scalars: override wins diff --git a/src/scripts/resolve_customization.py b/src/scripts/resolve_customization.py index 3299e1ade..5e7430ca1 100755 --- a/src/scripts/resolve_customization.py +++ b/src/scripts/resolve_customization.py @@ -11,12 +11,14 @@ Skill name is derived from the basename of the skill directory. Outputs merged JSON to stdout. Errors go to stderr. -Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`, -no virtualenv — plain `python3` is sufficient. +Uses only the Python stdlib (`tomllib`) — no third-party dependencies. +BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable +interpreter for you); a plain `python3` on PATH still works during the +transition. Either runner needs Python 3.11+ for `tomllib`. - python3 resolve_customization.py --skill /abs/path/to/skill-dir - python3 resolve_customization.py --skill ... --key agent - python3 resolve_customization.py --skill ... --key agent.menu + uv run resolve_customization.py --skill /abs/path/to/skill-dir + uv run resolve_customization.py --skill ... --key agent + uv run resolve_customization.py --skill ... --key agent.menu Merge rules (purely structural — no field-name special-casing): - Scalars (string, int, bool, float): override wins diff --git a/test/test-installation-components.js b/test/test-installation-components.js index f511b4376..b1acea6e6 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3319,134 +3319,63 @@ async function runTests() { console.log(''); // ============================================================ - // Test Suite 46: Python environment check (version parsing + classification) + // Test Suite 46: uv environment check (version parsing + messaging) // ============================================================ - console.log(`${colors.yellow}Test Suite 46: python-check version parsing and classification${colors.reset}\n`); + console.log(`${colors.yellow}Test Suite 46: uv-check version parsing and messaging${colors.reset}\n`); try { - const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check'); + const { parseUvVersion, detectUv } = require('../tools/installer/core/uv-check'); // Version parsing - const v312 = parsePythonVersion('Python 3.12.1'); - assert(v312 && v312.major === 3 && v312.minor === 12 && v312.patch === 1, 'parses "Python 3.12.1"'); - const v311 = parsePythonVersion('Python 3.11.0\n'); - assert(v311 && v311.raw === '3.11.0', 'parses with trailing newline'); - const v2 = parsePythonVersion('\nPython 2.7.18'); - assert(v2 && v2.major === 2, 'parses Python 2 output (stderr-style)'); - const noPatch = parsePythonVersion('Python 3.13'); + const plain = parseUvVersion('uv 0.5.31'); + assert(plain && plain.major === 0 && plain.minor === 5 && plain.patch === 31, 'parses "uv 0.5.31"'); + const brew = parseUvVersion('uv 0.5.31 (Homebrew 2025-02-12)'); + assert(brew && brew.raw === '0.5.31', 'parses uv version with build suffix'); + const noPatch = parseUvVersion('uv 1.2'); assert(noPatch && noPatch.patch === 0, 'missing patch defaults to 0'); - assert(parsePythonVersion('') === null, 'empty output returns null'); - assert(parsePythonVersion('command not found: python3') === null, 'non-version output returns null'); - assert(parsePythonVersion(null) === null, 'null output returns null'); + assert(parseUvVersion('') === null, 'empty output returns null'); + assert(parseUvVersion('command not found: uv') === null, 'non-version output returns null'); + assert(parseUvVersion(null) === null, 'null output returns null'); - // Classification against feature requirements - assert(classifyPython({ major: 3, minor: 11 }) === 'full', '3.11 is full support (tomllib floor)'); - assert(classifyPython({ major: 3, minor: 13 }) === 'full', '3.13 is full support'); - assert(classifyPython({ major: 4, minor: 0 }) === 'full', 'hypothetical 4.0 is full support'); - assert(classifyPython({ major: 3, minor: 10 }) === 'partial', '3.10 is partial (memlog yes, tomllib no)'); - assert(classifyPython({ major: 3, minor: 8 }) === 'partial', '3.8 is partial (memlog floor)'); - assert(classifyPython({ major: 3, minor: 7 }) === 'unsupported', '3.7 is unsupported'); - assert(classifyPython({ major: 2, minor: 7 }) === 'unsupported', '2.7 is unsupported'); - assert(classifyPython(null) === 'none', 'no python is none'); + // Detection smoke test — must not throw; result is null or well-formed. + const detectedUv = detectUv(); + assert(detectedUv === null || typeof detectedUv.version.raw === 'string', 'detectUv returns null or a well-formed result'); - // Detection smoke test — must not throw, and if it finds a Python the - // result must be well-formed. (CI machines may or may not have Python.) - const detected = detectPython(); - assert( - detected === null || - (typeof detected.command === 'string' && - typeof detected.version.raw === 'string' && - typeof detected.isRuntimeCommand === 'boolean'), - 'detectPython returns null or a well-formed result', - ); - - // checkPythonEnvironment branch coverage — stub detection, prompts, and - // process.exit so the assertions are deterministic regardless of the - // machine's Python. python-check resolves detectPython via module.exports - // and prompts via the shared module object, so swapping properties works. - const pythonCheck = require('../tools/installer/core/python-check'); + // checkUvEnvironment branch coverage — stub detection + prompts so the + // assertions are deterministic regardless of whether uv is installed. + const uvCheck = require('../tools/installer/core/uv-check'); const promptsModule = require('../tools/installer/prompts'); - const real = { - detectPython: pythonCheck.detectPython, - log: promptsModule.log, - note: promptsModule.note, - select: promptsModule.select, - cancel: promptsModule.cancel, - exit: process.exit, - }; - const stub = (detectResult, selectAnswer) => { - const seen = { success: [], warn: [], info: [], note: [], select: [], cancel: [], exit: [] }; - pythonCheck.detectPython = () => detectResult; + const realUv = { detectUv: uvCheck.detectUv, log: promptsModule.log, note: promptsModule.note }; + const stubUv = (detectResult) => { + const seen = { success: [], warn: [], note: [] }; + uvCheck.detectUv = () => detectResult; promptsModule.log = { success: async (m) => void seen.success.push(m), warn: async (m) => void seen.warn.push(m), - info: async (m) => void seen.info.push(m), + info: async () => {}, error: async () => {}, }; promptsModule.note = async (m, t) => void seen.note.push(t || m); - promptsModule.select = async (opts) => { - seen.select.push(opts.message); - return selectAnswer; - }; - promptsModule.cancel = async (m) => void seen.cancel.push(m); - process.exit = (code) => { - seen.exit.push(code); - throw new Error('__stub_exit__'); - }; return seen; }; try { - const v = (major, minor, patch) => ({ major, minor, patch, raw: `${major}.${minor}.${patch}` }); + // Branch: uv present — success, no warning. + let seen = stubUv({ version: { major: 0, minor: 5, patch: 31, raw: '0.5.31' } }); + let result = await uvCheck.checkUvEnvironment(); + assert(result.status === 'found' && seen.success.length === 1, 'uv present logs success'); + assert(seen.success[0].includes('uv run') && seen.warn.length === 0, 'uv present mentions uv run, no warning'); - // Branch: full support via the runtime command — success, no prompt. - let seen = stub({ command: 'python3', version: v(3, 12, 1), isRuntimeCommand: true }, 'continue'); - let result = await pythonCheck.checkPythonEnvironment(); - assert(result.status === 'full' && seen.success.length === 1, 'full support via python3 logs success'); - assert(seen.select.length === 0 && seen.warn.length === 0, 'full support via python3 skips warning and ack prompt'); - - // Branch: modern Python found, but not as `python3` — runtime mismatch. - seen = stub({ command: 'py -3', version: v(3, 12, 0), isRuntimeCommand: false }, 'continue'); - result = await pythonCheck.checkPythonEnvironment(); - assert(seen.success.length === 0, 'python3-mismatch never reports full support'); - assert( - seen.warn.length === 1 && seen.warn[0].includes('python3') && seen.warn[0].includes('py -3'), - 'python3-mismatch warns that scripts invoke python3', - ); - assert(seen.select.length === 1 && result.status === 'full', 'python3-mismatch still requires the ack prompt'); - - // Branch: partial support (3.8–3.10) — warn + ack, continue returns. - seen = stub({ command: 'python3', version: v(3, 9, 5), isRuntimeCommand: true }, 'continue'); - result = await pythonCheck.checkPythonEnvironment(); - assert( - result.status === 'partial' && seen.warn.length === 1 && seen.warn[0].includes('3.11+'), - 'partial support warns about tomllib floor', - ); - assert(seen.select.length === 1 && seen.exit.length === 0, 'partial support prompts and continue proceeds'); - - // Branch: no Python, non-interactive — warn + info, never prompts. - seen = stub(null, 'continue'); - result = await pythonCheck.checkPythonEnvironment({ nonInteractive: true }); - assert(result.status === 'none' && seen.warn[0].includes('No Python found'), 'non-interactive with no Python warns'); - assert(seen.select.length === 0 && seen.info.length === 1, 'non-interactive skips the ack prompt and logs continuation'); - - // Branch: no Python, interactive, user quits — cancel message + exit 0. - seen = stub(null, 'quit'); - let threw = false; - try { - await pythonCheck.checkPythonEnvironment(); - } catch (error) { - threw = error.message === '__stub_exit__'; - } - assert(threw && seen.exit.length === 1 && seen.exit[0] === 0, 'quit choice exits 0 (user-cancel convention)'); - assert(seen.cancel.length === 1, 'quit choice shows the cancel guidance'); + // Branch: uv missing — warn + setup note, never blocks (no prompt). + seen = stubUv(null); + result = await uvCheck.checkUvEnvironment(); + assert(result.status === 'missing' && seen.warn.length === 1, 'uv missing warns'); + assert(seen.warn[0].includes('de facto standard'), 'uv-missing warning frames uv as the de facto standard'); + assert(seen.note.length === 1 && seen.note[0].includes('uv'), 'uv missing shows a setup note'); } finally { - pythonCheck.detectPython = real.detectPython; - promptsModule.log = real.log; - promptsModule.note = real.note; - promptsModule.select = real.select; - promptsModule.cancel = real.cancel; - process.exit = real.exit; + uvCheck.detectUv = realUv.detectUv; + promptsModule.log = realUv.log; + promptsModule.note = realUv.note; } } catch (error) { console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`); diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 9c6c6cb6c..61c3408de 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -1233,6 +1233,9 @@ class Installer { ` 1. Launch your AI agent from your project folder`, ` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`, '', + ` ${color.cyan('Tip:')} BMAD workflows increasingly run Python scripts via ${color.cyan('uv run')} — uv is`, + ` becoming the de facto standard. If you don't have it yet, ask your agent to set it up.`, + '', ` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`, ` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`, ); diff --git a/tools/installer/core/python-check.js b/tools/installer/core/python-check.js deleted file mode 100644 index d8539cac3..000000000 --- a/tools/installer/core/python-check.js +++ /dev/null @@ -1,199 +0,0 @@ -const { spawnSync } = require('node:child_process'); -const prompts = require('../prompts'); - -// Python 3.11 added stdlib `tomllib` (PEP 680), which the shared scripts in -// src/scripts/ (resolve_config.py, resolve_customization.py) require to read -// BMAD's TOML config files. memlog.py is more lenient and runs on 3.8+. -const PYTHON_FULL_SUPPORT = { major: 3, minor: 11 }; -const PYTHON_PARTIAL_SUPPORT = { major: 3, minor: 8 }; - -// Every runtime call site (skill steps, on_complete hooks) invokes a literal -// `python3`, so only that command's version vouches for BMAD features. The -// fallback probes exist to tell the user "Python is installed, but not under -// the name BMAD uses" instead of a misleading "No Python found". -const RUNTIME_COMMAND = 'python3'; -const PROBE_CANDIDATES = - process.platform === 'win32' - ? [ - { command: 'python3', args: ['--version'] }, - { command: 'py', args: ['-3', '--version'] }, - { command: 'python', args: ['--version'] }, - ] - : [ - { command: 'python3', args: ['--version'] }, - { command: 'python', args: ['--version'] }, - ]; - -/** - * Parse a `python --version` output line into version parts. - * Python 3 prints to stdout; Python 2 printed to stderr — callers pass both. - * @param {string} output - Combined stdout/stderr from `python --version` - * @returns {{major: number, minor: number, patch: number, raw: string}|null} - */ -function parsePythonVersion(output) { - if (!output) return null; - const match = output.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/); - if (!match) return null; - return { - major: Number(match[1]), - minor: Number(match[2]), - patch: Number(match[3] || 0), - raw: `${match[1]}.${match[2]}.${match[3] || 0}`, - }; -} - -/** - * Classify a detected Python version against BMAD's feature requirements. - * @param {{major: number, minor: number}|null} version - * @returns {'full'|'partial'|'unsupported'|'none'} - */ -function classifyPython(version) { - if (!version) return 'none'; - const { major, minor } = version; - if (major > PYTHON_FULL_SUPPORT.major || (major === PYTHON_FULL_SUPPORT.major && minor >= PYTHON_FULL_SUPPORT.minor)) { - return 'full'; - } - if (major === PYTHON_PARTIAL_SUPPORT.major && minor >= PYTHON_PARTIAL_SUPPORT.minor) { - return 'partial'; - } - return 'unsupported'; -} - -/** - * Run one probe candidate and return its parsed version, or null. - * @param {{command: string, args: string[]}} candidate - * @returns {{major: number, minor: number, patch: number, raw: string}|null} - */ -function probeVersion(candidate) { - const run = (extra = {}) => - spawnSync(candidate.command, candidate.args, { - encoding: 'utf8', - timeout: 5000, - windowsHide: true, - ...extra, - }); - let result = run(); - // Node >=18.20/20.12 refuses to spawn .bat/.cmd without a shell - // (CVE-2024-27980 hardening) and reports EINVAL — pyenv-win ships its - // python shims as .bat. Args here are static literals, so a shell retry - // is injection-safe. - if (result.error && result.error.code === 'EINVAL' && process.platform === 'win32') { - result = run({ shell: true }); - } - if (result.error) return null; - return parsePythonVersion(`${result.stdout || ''}\n${result.stderr || ''}`); -} - -/** - * Probe the local environment for a Python interpreter. - * Tries each candidate command and returns the first that reports a version. - * `isRuntimeCommand` is true only when the match is `python3` — the command - * BMAD scripts actually invoke. - * @returns {{command: string, version: {major: number, minor: number, patch: number, raw: string}, isRuntimeCommand: boolean}|null} - */ -function detectPython() { - for (const candidate of PROBE_CANDIDATES) { - try { - const version = probeVersion(candidate); - if (version) { - const display = candidate.args.length > 1 ? `${candidate.command} ${candidate.args.slice(0, -1).join(' ')}` : candidate.command; - return { command: display, version, isRuntimeCommand: candidate.command === RUNTIME_COMMAND }; - } - } catch { - // Candidate not runnable — try the next one. - } - } - return null; -} - -function upgradeHints() { - return [ - 'How to get Python 3.11+ (as `python3`):', - ' macOS: brew install python3', - ' Windows: winget install Python.Python.3.12 (then ensure `python3` resolves, e.g. enable the python3 alias)', - ' Linux/WSL: sudo apt install python3 (Ubuntu 24.04+ ships 3.12; older distros: use pyenv or deadsnakes)', - ' Docker: add python3 to your image (e.g. apk add python3 / apt-get install -y python3)', - ].join('\n'); -} - -/** - * Check the local Python environment and warn about degraded BMAD features. - * - * Warn-don't-block: most of BMAD works without Python, so the install always - * may proceed — but the user must explicitly acknowledge the warning so it - * can't scroll past unseen. In non-interactive runs (--yes, or stdin is not - * a TTY) the warning is logged and the install continues without a prompt. - * - * @param {Object} [options] - * @param {boolean} [options.nonInteractive=false] - Skip the ack prompt (--yes, or no TTY) - * @returns {Promise<{status: string, detected: Object|null}>} - */ -async function checkPythonEnvironment({ nonInteractive = false } = {}) { - // Called via module.exports so tests can stub detection. - const detected = module.exports.detectPython(); - const status = classifyPython(detected ? detected.version : null); - - if (status === 'full' && detected.isRuntimeCommand) { - await prompts.log.success(`Python ${detected.version.raw} detected (${detected.command}) — all BMAD features supported.`); - return { status, detected }; - } - - if (detected && !detected.isRuntimeCommand) { - await prompts.log.warn( - `Python ${detected.version.raw} found via \`${detected.command}\`, but BMAD scripts invoke \`python3\`, which is not on PATH.\n` + - `Python-powered features (memlog session memory, TOML config resolution) won't run until \`python3\` resolves —\n` + - `add a python3 alias/shim, or reinstall Python with the python3 launcher enabled.`, - ); - } else if (status === 'partial') { - await prompts.log.warn( - `Python ${detected.version.raw} detected (${detected.command}) — BMAD's TOML config tools need Python 3.11+ (stdlib tomllib).\n` + - `Works: memlog session memory. Won't work: config/customization resolution scripts.`, - ); - } else { - const found = - status === 'unsupported' ? `Python ${detected.version.raw} detected (${detected.command}) — too old.` : 'No Python found on PATH.'; - await prompts.log.warn( - `${found} BMAD installs fine without it, but Python-powered features\n` + - `(memlog session memory, TOML config resolution) won't run until Python 3.11+ is available.`, - ); - } - await prompts.note(upgradeHints(), 'Python 3.11+ recommended'); - - if (nonInteractive) { - await prompts.log.info('Continuing anyway (non-interactive run). You can fix Python later — no reinstall needed.'); - return { status, detected }; - } - - const choice = await prompts.select({ - message: "BMAD's Python-powered features won't work yet. How do you want to proceed?", - choices: [ - { - name: 'Continue install', - value: 'continue', - hint: 'BMAD works without Python — you can fix Python later, no reinstall needed', - }, - { - name: 'Quit and fix Python first', - value: 'quit', - hint: 'make Python 3.11+ available as python3, then re-run the installer', - }, - ], - default: 'continue', - }); - - if (choice === 'quit') { - await prompts.cancel('Make Python 3.11+ available as `python3` (see hints above), then re-run the installer.'); - process.exit(0); - } - - return { status, detected }; -} - -module.exports = { - checkPythonEnvironment, - detectPython, - parsePythonVersion, - classifyPython, - PYTHON_FULL_SUPPORT, - PYTHON_PARTIAL_SUPPORT, -}; diff --git a/tools/installer/core/uv-check.js b/tools/installer/core/uv-check.js new file mode 100644 index 000000000..cddb096fd --- /dev/null +++ b/tools/installer/core/uv-check.js @@ -0,0 +1,97 @@ +const { spawnSync } = require('node:child_process'); +const prompts = require('../prompts'); + +// `uv` (https://docs.astral.sh/uv/) is becoming the de facto standard for +// running the Python scripts BMAD workflows shell out to: `uv run