diff --git a/docs/fr/how-to/non-interactive-installation.md b/docs/fr/how-to/non-interactive-installation.md index ee6ddad1c..bff76dac7 100644 --- a/docs/fr/how-to/non-interactive-installation.md +++ b/docs/fr/how-to/non-interactive-installation.md @@ -38,6 +38,7 @@ Nécessite [Node.js](https://nodejs.org) v20+ et `npx` (inclus avec npm). | `--communication-language ` | Langue de communication des agents | Anglais | | `--document-output-language ` | Langue de sortie des documents | Anglais | | `--output-folder ` | Chemin du dossier de sortie (voir les règles de résolution ci-dessous) | `_bmad-output` | +| `--scope ` | Nom de portée pour isoler les artefacts de planification/implémentation (voir ci-dessous) | _(vide - pas de portée)_ | #### Résolution du chemin du dossier de sortie @@ -46,10 +47,24 @@ La valeur passée à `--output-folder` (ou saisie de manière interactive) est r | Type d'entrée | Exemple | Résolu comme | |-------------------------------|----------------------------|--------------------------------------------------------------| | Chemin relatif (par défaut) | `_bmad-output` | `/_bmad-output` | -| Chemin relatif avec traversée | `../../shared-outputs` | Chemin absolu normalisé — ex. `/Users/me/shared-outputs` | -| Chemin absolu | `/Users/me/shared-outputs` | Utilisé tel quel — la racine du projet n'est **pas** ajoutée | +| Chemin relatif avec traversée | `../../shared-outputs` | Chemin absolu normalisé - ex. `/Users/me/shared-outputs` | +| Chemin absolu | `/Users/me/shared-outputs` | Utilisé tel quel - la racine du projet n'est **pas** ajoutée | -Le chemin résolu est ce que les agents et les workflows vont utiliser lors de l'écriture des fichiers de sortie. L'utilisation d'un chemin absolu ou d'un chemin relatif avec traversée vous permet de diriger tous les artefacts générés vers un répertoire en dehors de l'arborescence de votre projet — utile pour les configurations partagées ou les monorepos. +Le chemin résolu est ce que les agents et les workflows vont utiliser lors de l'écriture des fichiers de sortie. L'utilisation d'un chemin absolu ou d'un chemin relatif avec traversée vous permet de diriger tous les artefacts générés vers un répertoire en dehors de l'arborescence de votre projet - utile pour les configurations partagées ou les monorepos. + +#### Portée + +Lorsque `--scope` est fourni, `planning_artifacts` et `implementation_artifacts` sont placés dans un sous-répertoire. Le dossier de sortie racine reste partagé pour le contexte projet et le brainstorming. + +``` +# Sans --scope : +_bmad-output/planning-artifacts/prd.md + +# Avec --scope admin-portal : +_bmad-output/admin-portal/planning-artifacts/prd.md +``` + +Le nom ne peut contenir que des lettres, chiffres, tirets, points ou underscores (`.` et `..` ne sont pas autorisés). L'omettre conserve la structure actuelle. ### Autres options @@ -60,16 +75,16 @@ Le chemin résolu est ce que les agents et les workflows vont utiliser lors de l ## IDs de modules -IDs de modules disponibles pour l’option `--modules` : +IDs de modules disponibles pour l'option `--modules` : -- `bmm` — méthode BMad Master -- `bmb` — BMad Builder +- `bmm` - méthode BMad Master +- `bmb` - BMad Builder Consultez le [registre BMad](https://github.com/bmad-code-org) pour les modules externes disponibles. ## IDs d'outils/IDE -IDs d'outils disponibles pour l’option `--tools` : +IDs d'outils disponibles pour l'option `--tools` : **Recommandés :** `claude-code`, `cursor` @@ -140,15 +155,15 @@ npx bmad-method install \ BMad valide toutes les options fournis : -- **Directory** — Doit être un chemin valide avec des permissions d'écriture -- **Modules** — Avertit des IDs de modules invalides (mais n'échoue pas) -- **Tools** — Avertit des IDs d'outils invalides (mais n'échoue pas) -- **Custom Content** — Chaque chemin doit contenir un fichier `module.yaml` valide -- **Action** — Doit être l'une des suivantes : `install`, `update`, `quick-update` +- **Directory** - Doit être un chemin valide avec des permissions d'écriture +- **Modules** - Avertit des IDs de modules invalides (mais n'échoue pas) +- **Tools** - Avertit des IDs d'outils invalides (mais n'échoue pas) +- **Custom Content** - Chaque chemin doit contenir un fichier `module.yaml` valide +- **Action** - Doit être l'une des suivantes : `install`, `update`, `quick-update` Les valeurs invalides entraîneront soit : -1. L’affichage d’un message d'erreur suivi d’un exit (pour les options critiques comme le répertoire) -2. Un avertissement puis la continuation de l’installation (pour les éléments optionnels comme le contenu personnalisé) +1. L'affichage d'un message d'erreur suivi d'un exit (pour les options critiques comme le répertoire) +2. Un avertissement puis la continuation de l'installation (pour les éléments optionnels comme le contenu personnalisé) 3. Un retour aux invites interactives (pour les valeurs requises manquantes) :::tip[Bonnes pratiques] diff --git a/docs/how-to/non-interactive-installation.md b/docs/how-to/non-interactive-installation.md index 64687c0a1..0c3dcf7c5 100644 --- a/docs/how-to/non-interactive-installation.md +++ b/docs/how-to/non-interactive-installation.md @@ -38,6 +38,7 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm). | `--communication-language ` | Agent communication language | English | | `--document-output-language ` | Document output language | English | | `--output-folder ` | Output folder path (see resolution rules below) | `_bmad-output` | +| `--scope ` | Scope name to isolate planning/implementation artifacts (see below) | _(empty — no scope)_ | #### Output Folder Path Resolution @@ -51,6 +52,20 @@ The value passed to `--output-folder` (or entered interactively) is resolved acc The resolved path is what agents and workflows use at runtime when writing output files. Using an absolute path or a traversal-based relative path lets you direct all generated artifacts to a directory outside your project tree — useful for shared or monorepo setups. +#### Scope + +When `--scope` is provided, `planning_artifacts` and `implementation_artifacts` are placed under a scoped subdirectory. The output folder root stays shared for project-context and brainstorming. + +``` +# Without --scope: +_bmad-output/planning-artifacts/prd.md + +# With --scope admin-portal: +_bmad-output/admin-portal/planning-artifacts/prd.md +``` + +The name must contain only letters, numbers, hyphens, dots, or underscores (`.` and `..` are not allowed). Omitting it preserves the current flat structure. + ### Other Options | Flag | Description | diff --git a/docs/zh-cn/how-to/non-interactive-installation.md b/docs/zh-cn/how-to/non-interactive-installation.md index df7259d97..7486fcb14 100644 --- a/docs/zh-cn/how-to/non-interactive-installation.md +++ b/docs/zh-cn/how-to/non-interactive-installation.md @@ -38,6 +38,21 @@ sidebar: | `--communication-language ` | 智能体通信语言 | 英语 | | `--document-output-language ` | 文档输出语言 | 英语 | | `--output-folder ` | 输出文件夹路径 | _bmad-output | +| `--scope ` | 范围名称,用于隔离规划/实施产物(见下文) | _(空 — 无范围)_ | + +#### 范围 + +提供 `--scope` 时,`planning_artifacts` 和 `implementation_artifacts` 会放在范围子目录下。输出文件夹根目录保持共享。 + +``` +# 不带 --scope: +_bmad-output/planning-artifacts/prd.md + +# 带 --scope admin-portal: +_bmad-output/admin-portal/planning-artifacts/prd.md +``` + +名称只能包含字母、数字、连字符、点或下划线(不允许 `.` 和 `..`)。省略时保持当前的平面结构。 ### 其他选项 diff --git a/src/core-skills/module.yaml b/src/core-skills/module.yaml index 48e7a58f7..9c84db20d 100644 --- a/src/core-skills/module.yaml +++ b/src/core-skills/module.yaml @@ -23,3 +23,9 @@ output_folder: prompt: "Where should output files be saved?" default: "_bmad-output" result: "{project-root}/{value}" + +scope: + prompt: "Scope name to keep outputs separate (e.g., admin-portal). Enter to skip:" + default: "" + regex: "^$|^[a-zA-Z0-9._-]*[a-zA-Z0-9][a-zA-Z0-9._-]*$" + result: "{value}" diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 4e5fa7282..ab0f8fedf 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1713,6 +1713,128 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 33: scope isolates planning/implementation artifacts + // ============================================================ + console.log(`${colors.yellow}Test Suite 33: Scoped Output Isolation${colors.reset}\n`); + + let tempBmadDir33; + try { + const { Installer } = require('../tools/installer/core/installer'); + const testInstaller = new Installer(); + const yaml = require('yaml'); + + tempBmadDir33 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-scope-')); + const coreDir33 = path.join(tempBmadDir33, 'core'); + const bmmDir33 = path.join(tempBmadDir33, 'bmm'); + await fs.ensureDir(coreDir33); + await fs.ensureDir(bmmDir33); + + // --- Scoped install --- + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { + user_name: 'Test', + output_folder: '{project-root}/_bmad-output', + scope: 'admin-portal', + }, + bmm: { + project_name: 'TestProject', + planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', + implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', + }, + }); + + const core33 = yaml.parse(await fs.readFile(path.join(coreDir33, 'config.yaml'), 'utf8')); + const bmm33 = yaml.parse(await fs.readFile(path.join(bmmDir33, 'config.yaml'), 'utf8')); + + assert(core33.output_folder === '{project-root}/_bmad-output', 'Core output_folder is NOT scoped'); + assert(core33.scope === 'admin-portal', 'Core stores scope separately'); + assert(bmm33.planning_artifacts === '{project-root}/_bmad-output/admin-portal/planning-artifacts', 'BMM planning_artifacts is scoped'); + assert( + bmm33.implementation_artifacts === '{project-root}/_bmad-output/admin-portal/implementation-artifacts', + 'BMM implementation_artifacts is scoped', + ); + assert(bmm33.output_folder === '{project-root}/_bmad-output', 'BMM output_folder stays as shared root'); + assert(bmm33.scope === 'admin-portal', 'BMM inherits scope from core spread'); + + // --- Empty scope (backward compat) --- + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { user_name: 'Test', output_folder: '{project-root}/_bmad-output', scope: '' }, + bmm: { + project_name: 'TestProject', + planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', + implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', + }, + }); + + const bmmEmpty = yaml.parse(await fs.readFile(path.join(bmmDir33, 'config.yaml'), 'utf8')); + assert( + bmmEmpty.planning_artifacts === '{project-root}/_bmad-output/planning-artifacts', + 'Empty scope preserves original planning_artifacts', + ); + + // --- Missing scope key (backward compat) --- + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { user_name: 'Test', output_folder: '{project-root}/_bmad-output' }, + bmm: { + project_name: 'TestProject', + planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', + }, + }); + + const bmmMissing = yaml.parse(await fs.readFile(path.join(bmmDir33, 'config.yaml'), 'utf8')); + assert( + bmmMissing.planning_artifacts === '{project-root}/_bmad-output/planning-artifacts', + 'Missing scope key preserves original planning_artifacts', + ); + + // --- Scoped with raw output_folder (no {project-root} prefix, matches --yes path) --- + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { user_name: 'Test', output_folder: '_bmad-output', scope: 'phase-2' }, + bmm: { + project_name: 'TestProject', + planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', + implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', + }, + }); + + const bmmRaw = yaml.parse(await fs.readFile(path.join(bmmDir33, 'config.yaml'), 'utf8')); + assert( + bmmRaw.planning_artifacts === '{project-root}/_bmad-output/phase-2/planning-artifacts', + 'Raw output_folder (--yes path) still scopes correctly', + ); + + // --- Dot-segment scope rejected --- + let dotSegmentThrew = false; + try { + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { user_name: 'Test', output_folder: '{project-root}/_bmad-output', scope: '..' }, + bmm: { project_name: 'TestProject', planning_artifacts: '{project-root}/_bmad-output/planning-artifacts' }, + }); + } catch (error) { + dotSegmentThrew = error.message.includes('dot-segments'); + } + assert(dotSegmentThrew, 'Dot-segment scope ".." is rejected'); + + // --- Custom path not under output_folder (scope bypassed) --- + await testInstaller.generateModuleConfigs(tempBmadDir33, { + core: { user_name: 'Test', output_folder: '{project-root}/_bmad-output', scope: 'test' }, + bmm: { + project_name: 'TestProject', + planning_artifacts: '{project-root}/custom-folder/planning', + }, + }); + + const bmmCustom = yaml.parse(await fs.readFile(path.join(bmmDir33, 'config.yaml'), 'utf8')); + assert(bmmCustom.planning_artifacts === '{project-root}/custom-folder/planning', 'Custom path not under output_folder bypasses scope'); + } catch (error) { + assert(false, `Scoped output test error: ${error.message}`); + } finally { + if (tempBmadDir33) await fs.remove(tempBmadDir33).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/commands/install.js b/tools/installer/commands/install.js index 96f536ef4..4e0423ba6 100644 --- a/tools/installer/commands/install.js +++ b/tools/installer/commands/install.js @@ -23,6 +23,7 @@ module.exports = { ['--communication-language ', 'Language for agent communication (default: English)'], ['--document-output-language ', 'Language for document output (default: English)'], ['--output-folder ', 'Output folder path relative to project root (default: _bmad-output)'], + ['--scope ', 'Scope name to isolate planning/implementation artifacts (e.g., admin-portal)'], ['-y, --yes', 'Accept all defaults and skip prompts where possible'], ], action: async (options) => { diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 111c88b54..9b85b3ab1 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -823,6 +823,20 @@ class Installer { // Create a comment section to identify core values coreSection = '\n# Core Configuration Values\n'; + + // When scope is set, insert it after output_folder in artifact paths + if (finalConfig.scope && finalConfig.output_folder) { + if (finalConfig.scope === '.' || finalConfig.scope === '..') { + throw new Error(`Invalid scope "${finalConfig.scope}": dot-segments are not allowed`); + } + const folder = finalConfig.output_folder.replace(/[\\/]+$/, ''); + for (const key of ['planning_artifacts', 'implementation_artifacts']) { + // Match only at a directory boundary (followed by / or end of string) + if (finalConfig[key]?.includes(`${folder}/`)) { + finalConfig[key] = finalConfig[key].replace(`${folder}/`, `${folder}/${finalConfig.scope}/`); + } + } + } } // Clean the config to remove any non-serializable values (like functions) diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 03d38e4da..0e6ed0dc7 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -713,7 +713,7 @@ class UI { const configCollector = new OfficialModules(); // Seed core config from CLI options if provided - if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) { + if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder || options.scope) { const coreConfig = {}; if (options.userName) { coreConfig.user_name = options.userName; @@ -731,10 +731,36 @@ class UI { coreConfig.output_folder = options.outputFolder; await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`); } + if (options.scope) { + if (!/^[a-zA-Z0-9._-]+$/.test(options.scope) || options.scope === '.' || options.scope === '..') { + await prompts.log.error( + `Invalid scope "${options.scope}": use only letters, numbers, hyphens, dots, or underscores (not "." or "..")`, + ); + process.exit(1); + } + coreConfig.scope = options.scope; + await prompts.log.info(`Using scope from command-line: ${options.scope}`); + } // Load existing config to merge with provided options await configCollector.loadExistingConfig(directory); const existingConfig = configCollector.collectedConfig.core || {}; + // When --yes, fill in defaults for core values not provided via CLI + if (options.yes) { + if (!existingConfig.user_name) { + let safeUsername; + try { + safeUsername = os.userInfo().username; + } catch { + safeUsername = process.env.USER || process.env.USERNAME || 'User'; + } + existingConfig.user_name = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); + } + existingConfig.communication_language = existingConfig.communication_language || 'English'; + existingConfig.document_output_language = existingConfig.document_output_language || 'English'; + existingConfig.output_folder = existingConfig.output_folder || '_bmad-output'; + existingConfig.scope = existingConfig.scope || ''; + } configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig }; // If not all options are provided, collect the missing ones interactively (unless --yes flag) @@ -743,6 +769,7 @@ class UI { (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder) ) { await configCollector.collectModuleConfig('core', directory, false, true); + Object.assign(configCollector.collectedConfig.core, coreConfig); } } else if (options.yes) { // Use all defaults when --yes flag is set @@ -762,6 +789,7 @@ class UI { communication_language: 'English', document_output_language: 'English', output_folder: '_bmad-output', + scope: '', }; await prompts.log.info('Using default configuration (--yes flag)'); }