Compare commits
5 Commits
f02d36066d
...
62b2f0a891
| Author | SHA1 | Date |
|---|---|---|
|
|
62b2f0a891 | |
|
|
2c5436f672 | |
|
|
1f99eb0496 | |
|
|
36f9df69bf | |
|
|
4655bb1482 |
|
|
@ -1,4 +1,4 @@
|
|||
# Step 8: Scoping Exercise - MVP & Future Features
|
||||
# Step 8: Scoping Exercise - Scope Definition (Phased or Single-Release)
|
||||
|
||||
**Progress: Step 8 of 11** - Next: Functional Requirements
|
||||
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
- 📋 YOU ARE A FACILITATOR, not a content generator
|
||||
- 💬 FOCUS on strategic scope decisions that keep projects viable
|
||||
- 🎯 EMPHASIZE lean MVP thinking while preserving long-term vision
|
||||
- ⚠️ NEVER de-scope, defer, or phase out requirements that the user explicitly included in their input documents without asking first
|
||||
- ⚠️ NEVER invent phasing (MVP/Growth/Vision) unless the user requests phased delivery — if input documents define all components as core requirements, they are ALL in scope
|
||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||
- ✅ YOU MUST ALWAYS WRITE all artifact and document content in `{document_output_language}`
|
||||
|
||||
|
|
@ -34,7 +36,7 @@
|
|||
|
||||
## YOUR TASK:
|
||||
|
||||
Conduct comprehensive scoping exercise to define MVP boundaries and prioritize features across development phases.
|
||||
Conduct comprehensive scoping exercise to define release boundaries and prioritize features based on the user's chosen delivery mode (phased or single-release).
|
||||
|
||||
## SCOPING SEQUENCE:
|
||||
|
||||
|
|
@ -75,30 +77,41 @@ Use structured decision-making for scope:
|
|||
- Advanced functionality that builds on MVP
|
||||
- Ask what features could be added in versions 2, 3, etc.
|
||||
|
||||
**⚠️ SCOPE CHANGE CONFIRMATION GATE:**
|
||||
- If you believe any user-specified requirement should be deferred or de-scoped, you MUST present this to the user and get explicit confirmation BEFORE removing it from scope
|
||||
- Frame it as a recommendation, not a decision: "I'd recommend deferring X because [reason]. Do you agree, or should it stay in scope?"
|
||||
- NEVER silently move user requirements to a later phase or exclude them from MVP
|
||||
- Before creating any consequential phase-based artifacts (e.g., phase tags, labels, or follow-on prompts), present artifact creation as a recommendation and proceed only after explicit user approval
|
||||
|
||||
### 4. Progressive Feature Roadmap
|
||||
|
||||
Create phased development approach:
|
||||
- Guide mapping of features across development phases
|
||||
- Structure as Phase 1 (MVP), Phase 2 (Growth), Phase 3 (Vision)
|
||||
- Ensure clear progression and dependencies
|
||||
**CRITICAL: Phasing is NOT automatic. Check the user's input first.**
|
||||
|
||||
- Core user value delivery
|
||||
- Essential user journeys
|
||||
- Basic functionality that works reliably
|
||||
Before proposing any phased approach, review the user's input documents:
|
||||
|
||||
**Phase 2: Growth**
|
||||
- **If the input documents define all components as core requirements with no mention of phases:** Present all requirements as a single release scope. Do NOT invent phases or move requirements to fabricated future phases.
|
||||
- **If the input documents explicitly request phased delivery:** Guide mapping of features across the phases the user defined.
|
||||
- **If scope is unclear:** ASK the user whether they want phased delivery or a single release before proceeding.
|
||||
|
||||
- Additional user types
|
||||
- Enhanced features
|
||||
- Scale improvements
|
||||
**When the user requests phased delivery**, guide mapping of features across the phases the user defines:
|
||||
|
||||
**Phase 3: Expansion**
|
||||
- Use user-provided phase labels and count; if none are provided, propose a default (e.g., MVP/Growth/Vision) and ask for confirmation
|
||||
- Ensure clear progression and dependencies between phases
|
||||
|
||||
- Advanced capabilities
|
||||
- Platform features
|
||||
- New markets or use cases
|
||||
**Each phase should address:**
|
||||
|
||||
**Where does your current vision fit in this development sequence?**"
|
||||
- Core user value delivery and essential journeys for that phase
|
||||
- Clear boundaries on what ships in each phase
|
||||
- Dependencies on prior phases
|
||||
|
||||
**When the user chooses a single release**, define the complete scope:
|
||||
|
||||
- All user-specified requirements are in scope
|
||||
- Focus must-have vs nice-to-have analysis on what ships in this release
|
||||
- Do NOT create phases — use must-have/nice-to-have priority within the single release
|
||||
|
||||
**If phased delivery:** "Where does your current vision fit in this development sequence?"
|
||||
**If single release:** "How does your current vision map to this upcoming release?"
|
||||
|
||||
### 5. Risk-Based Scoping
|
||||
|
||||
|
|
@ -129,6 +142,8 @@ Prepare comprehensive scoping section:
|
|||
|
||||
#### Content Structure:
|
||||
|
||||
**If user chose phased delivery:**
|
||||
|
||||
```markdown
|
||||
## Project Scoping & Phased Development
|
||||
|
||||
|
|
@ -160,11 +175,39 @@ Prepare comprehensive scoping section:
|
|||
**Resource Risks:** {{contingency_approach}}
|
||||
```
|
||||
|
||||
**If user chose single release (no phasing):**
|
||||
|
||||
```markdown
|
||||
## Project Scoping
|
||||
|
||||
### Strategy & Philosophy
|
||||
|
||||
**Approach:** {{chosen_approach}}
|
||||
**Resource Requirements:** {{team_size_and_skills}}
|
||||
|
||||
### Complete Feature Set
|
||||
|
||||
**Core User Journeys Supported:**
|
||||
{{all_journeys}}
|
||||
|
||||
**Must-Have Capabilities:**
|
||||
{{list_of_must_have_features}}
|
||||
|
||||
**Nice-to-Have Capabilities:**
|
||||
{{list_of_nice_to_have_features}}
|
||||
|
||||
### Risk Mitigation Strategy
|
||||
|
||||
**Technical Risks:** {{mitigation_approach}}
|
||||
**Market Risks:** {{validation_approach}}
|
||||
**Resource Risks:** {{contingency_approach}}
|
||||
```
|
||||
|
||||
### 7. Present MENU OPTIONS
|
||||
|
||||
Present the scoping decisions for review, then display menu:
|
||||
- Show strategic scoping plan (using structure from step 6)
|
||||
- Highlight MVP boundaries and phased roadmap
|
||||
- Highlight release boundaries and prioritization (phased roadmap only if phased delivery was selected)
|
||||
- Ask if they'd like to refine further, get other perspectives, or proceed
|
||||
- Present menu options naturally as part of conversation
|
||||
|
||||
|
|
@ -173,7 +216,7 @@ Display: "**Select:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Fu
|
|||
#### Menu Handling Logic:
|
||||
- IF A: Invoke the `bmad-advanced-elicitation` skill with the current scoping analysis, process the enhanced insights that come back, ask user if they accept the improvements, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
||||
- IF P: Invoke the `bmad-party-mode` skill with the scoping context, process the collaborative insights on MVP and roadmap decisions, ask user if they accept the changes, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
||||
- IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array, then read fully and follow: ./step-09-functional.md
|
||||
- IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array (also add `releaseMode: phased` or `releaseMode: single-release` to frontmatter based on user's choice), then read fully and follow: ./step-09-functional.md
|
||||
- IF Any other: help user respond, then redisplay menu
|
||||
|
||||
#### EXECUTION RULES:
|
||||
|
|
@ -189,8 +232,9 @@ When user selects 'C', append the content directly to the document using the str
|
|||
|
||||
✅ Complete PRD document analyzed for scope implications
|
||||
✅ Strategic MVP approach defined and justified
|
||||
✅ Clear MVP feature boundaries established
|
||||
✅ Phased development roadmap created
|
||||
✅ Clear feature boundaries established (phased or single-release, per user preference)
|
||||
✅ All user-specified requirements accounted for — none silently removed or deferred
|
||||
✅ Any scope reduction recommendations presented to user with rationale and explicit confirmation obtained
|
||||
✅ Key risks identified and mitigation strategies defined
|
||||
✅ User explicitly agrees to scope decisions
|
||||
✅ A/P/C menu presented and handled correctly
|
||||
|
|
@ -202,8 +246,11 @@ When user selects 'C', append the content directly to the document using the str
|
|||
❌ Making scope decisions without strategic rationale
|
||||
❌ Not getting explicit user agreement on MVP boundaries
|
||||
❌ Missing critical risk analysis
|
||||
❌ Not creating clear phased development approach
|
||||
❌ Not presenting A/P/C menu after content generation
|
||||
❌ **CRITICAL**: Silently de-scoping or deferring requirements that the user explicitly included in their input documents
|
||||
❌ **CRITICAL**: Inventing phasing (MVP/Growth/Vision) when the user did not request phased delivery
|
||||
❌ **CRITICAL**: Making consequential scoping decisions (what is in/out of scope) without explicit user confirmation
|
||||
❌ **CRITICAL**: Creating phase-based artifacts (tags, labels, follow-on prompts) without explicit user approval
|
||||
|
||||
❌ **CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions
|
||||
❌ **CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ Make targeted improvements:
|
|||
- All user success criteria
|
||||
- All functional requirements (capability contract)
|
||||
- All user journey narratives
|
||||
- All scope decisions (MVP, Growth, Vision)
|
||||
- All scope decisions (whether phased or single-release), including consent-critical evidence (explicit user confirmations and rationales for any scope changes from step 8)
|
||||
- All non-functional requirements
|
||||
- Product differentiator and vision
|
||||
- Domain-specific requirements
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const { Installer } = require('../tools/installer/core/installer');
|
||||
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
|
||||
const { OfficialModules } = require('../tools/installer/modules/official-modules');
|
||||
const { IdeManager } = require('../tools/installer/ide/manager');
|
||||
const { clearCache, loadPlatformCodes } = require('../tools/installer/ide/platform-codes');
|
||||
|
||||
|
|
@ -126,6 +128,56 @@ async function createSkillCollisionFixture() {
|
|||
return { root: fixtureRoot, bmadDir: fixtureDir };
|
||||
}
|
||||
|
||||
async function createCustomModuleManifestFixture() {
|
||||
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-manifest-'));
|
||||
const bmadDir = path.join(fixtureRoot, '_bmad');
|
||||
const configDir = path.join(bmadDir, '_config');
|
||||
const moduleSourceDir = path.join(fixtureRoot, 'test-module-source');
|
||||
await fs.ensureDir(configDir);
|
||||
await fs.ensureDir(moduleSourceDir);
|
||||
|
||||
const minimalAgent = '<agent name="Test" title="T"><persona>p</persona></agent>';
|
||||
await fs.ensureDir(path.join(bmadDir, 'core', 'agents'));
|
||||
await fs.writeFile(path.join(bmadDir, 'core', 'agents', 'test.md'), minimalAgent);
|
||||
await fs.ensureDir(path.join(bmadDir, 'test-module', 'agents'));
|
||||
await fs.writeFile(path.join(bmadDir, 'test-module', 'agents', 'test.md'), minimalAgent);
|
||||
await fs.writeFile(path.join(moduleSourceDir, 'module.yaml'), ['code: test-module', 'name: Test Module', ''].join('\n'));
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(configDir, 'manifest.yaml'),
|
||||
[
|
||||
'installation:',
|
||||
' version: 6.2.2',
|
||||
' installDate: 2026-03-30T00:00:00.000Z',
|
||||
' lastUpdated: 2026-03-30T00:00:00.000Z',
|
||||
'modules:',
|
||||
' - name: core',
|
||||
' version: 6.2.2',
|
||||
' installDate: 2026-03-30T00:00:00.000Z',
|
||||
' lastUpdated: 2026-03-30T00:00:00.000Z',
|
||||
' source: built-in',
|
||||
' npmPackage: null',
|
||||
' repoUrl: null',
|
||||
' - name: test-module',
|
||||
' version: null',
|
||||
' installDate: 2026-03-30T00:00:00.000Z',
|
||||
' lastUpdated: 2026-03-30T00:00:00.000Z',
|
||||
' source: custom',
|
||||
' npmPackage: null',
|
||||
' repoUrl: null',
|
||||
'customModules:',
|
||||
' - id: test-module',
|
||||
' name: "Test Module"',
|
||||
` sourcePath: ${JSON.stringify(moduleSourceDir)}`,
|
||||
'ides:',
|
||||
' - codex',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
return { root: fixtureRoot, bmadDir, manifestPath: path.join(configDir, 'manifest.yaml'), moduleSourceDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite
|
||||
*/
|
||||
|
|
@ -1713,6 +1765,107 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Suite 33: Main manifest preserves active customModules only
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 33: Preserve active customModules in main manifest${colors.reset}\n`);
|
||||
|
||||
let customManifestFixture = null;
|
||||
try {
|
||||
customManifestFixture = await createCustomModuleManifestFixture();
|
||||
const yaml = require('yaml');
|
||||
const originalManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8'));
|
||||
originalManifest.customModules.push({
|
||||
id: 'removed-module',
|
||||
name: 'Removed Module',
|
||||
sourcePath: path.join(customManifestFixture.root, 'removed-module-source'),
|
||||
});
|
||||
await fs.writeFile(customManifestFixture.manifestPath, yaml.stringify(originalManifest), 'utf8');
|
||||
|
||||
const generator33 = new ManifestGenerator();
|
||||
await generator33.generateManifests(customManifestFixture.bmadDir, ['core', 'test-module'], [], { ides: ['codex'] });
|
||||
|
||||
const updatedManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8'));
|
||||
const customModule = updatedManifest.customModules?.find((entry) => entry.id === 'test-module');
|
||||
|
||||
assert(Array.isArray(updatedManifest.customModules), 'Main manifest keeps customModules array');
|
||||
assert(customModule !== undefined, 'Main manifest preserves existing custom module entry');
|
||||
assert(
|
||||
customModule && customModule.sourcePath === customManifestFixture.moduleSourceDir,
|
||||
'Main manifest preserves custom module sourcePath',
|
||||
);
|
||||
assert(
|
||||
!updatedManifest.customModules?.some((entry) => entry.id === 'removed-module'),
|
||||
'Main manifest drops stale custom module entries',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Main manifest preserves customModules test succeeds', error.message);
|
||||
} finally {
|
||||
if (customManifestFixture?.root) await fs.remove(customManifestFixture.root).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Suite 34: Quick update uses manifest-backed custom sources
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 34: Quick update uses manifest-backed custom module sources${colors.reset}\n`);
|
||||
|
||||
let quickUpdateFixture = null;
|
||||
const originalListAvailable34 = OfficialModules.prototype.listAvailable;
|
||||
const originalLoadExistingConfig34 = OfficialModules.prototype.loadExistingConfig;
|
||||
const originalCollectModuleConfigQuick34 = OfficialModules.prototype.collectModuleConfigQuick;
|
||||
try {
|
||||
quickUpdateFixture = await createCustomModuleManifestFixture();
|
||||
const installer34 = new Installer();
|
||||
installer34.externalModuleManager.hasModule = async () => false;
|
||||
installer34.externalModuleManager.listAvailable = async () => [];
|
||||
|
||||
let capturedInstallConfig34 = null;
|
||||
installer34.install = async (config) => {
|
||||
capturedInstallConfig34 = config;
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
OfficialModules.prototype.listAvailable = async function () {
|
||||
return { modules: [], customModules: [] };
|
||||
};
|
||||
OfficialModules.prototype.loadExistingConfig = async function () {
|
||||
this.collectedConfig = this.collectedConfig || {};
|
||||
};
|
||||
OfficialModules.prototype.collectModuleConfigQuick = async function (moduleName) {
|
||||
this.collectedConfig = this.collectedConfig || {};
|
||||
if (!this.collectedConfig[moduleName]) {
|
||||
this.collectedConfig[moduleName] = {};
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
await installer34.quickUpdate({
|
||||
directory: quickUpdateFixture.root,
|
||||
skipPrompts: true,
|
||||
});
|
||||
|
||||
const customModule34 = capturedInstallConfig34?._customModuleSources?.get('test-module');
|
||||
|
||||
assert(capturedInstallConfig34 !== null, 'Quick update forwards config to install');
|
||||
assert(customModule34 !== undefined, 'Quick update keeps manifest-backed custom module updateable');
|
||||
assert(customModule34 && customModule34.cached === false, 'Quick update uses manifest-backed source before cache');
|
||||
assert(
|
||||
customModule34 && customModule34.sourcePath === quickUpdateFixture.moduleSourceDir,
|
||||
'Quick update uses preserved manifest sourcePath for custom modules',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Quick update manifest-backed custom source test succeeds', error.message);
|
||||
} finally {
|
||||
OfficialModules.prototype.listAvailable = originalListAvailable34;
|
||||
OfficialModules.prototype.loadExistingConfig = originalLoadExistingConfig34;
|
||||
OfficialModules.prototype.collectModuleConfigQuick = originalCollectModuleConfigQuick34;
|
||||
if (quickUpdateFixture?.root) await fs.remove(quickUpdateFixture.root).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1144,59 +1144,12 @@ class Installer {
|
|||
const configuredIdes = existingInstall.ides;
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
|
||||
// Get custom module sources: first from --custom-content (re-cache from source), then from cache
|
||||
const customModuleSources = new Map();
|
||||
if (config.customContent?.sources?.length > 0) {
|
||||
for (const source of config.customContent.sources) {
|
||||
if (source.id && source.path && (await fs.pathExists(source.path))) {
|
||||
customModuleSources.set(source.id, {
|
||||
id: source.id,
|
||||
name: source.name || source.id,
|
||||
sourcePath: source.path,
|
||||
cached: false, // From CLI, will be re-cached
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
|
||||
for (const cachedModule of cachedModules) {
|
||||
const moduleId = cachedModule.name;
|
||||
const cachedPath = path.join(cacheDir, moduleId);
|
||||
|
||||
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
||||
if (!(await fs.pathExists(cachedPath))) {
|
||||
continue;
|
||||
}
|
||||
if (!cachedModule.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we already have this module from manifest
|
||||
if (customModuleSources.has(moduleId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is an external official module - skip cache for those
|
||||
const isExternal = await this.externalModuleManager.hasModule(moduleId);
|
||||
if (isExternal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is actually a custom module (has module.yaml)
|
||||
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
customModuleSources.set(moduleId, {
|
||||
id: moduleId,
|
||||
name: moduleId,
|
||||
sourcePath: cachedPath,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const customModuleSources = await this.customModules.assembleQuickUpdateSources(
|
||||
config,
|
||||
existingInstall,
|
||||
bmadDir,
|
||||
this.externalModuleManager,
|
||||
);
|
||||
|
||||
// Get available modules (what we have source for)
|
||||
const availableModulesData = await new OfficialModules().listAvailable();
|
||||
|
|
|
|||
|
|
@ -377,10 +377,12 @@ class ManifestGenerator {
|
|||
*/
|
||||
async writeMainManifest(cfgDir) {
|
||||
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
||||
const installedModuleSet = new Set(this.modules);
|
||||
|
||||
// Read existing manifest to preserve install date
|
||||
let existingInstallDate = null;
|
||||
const existingModulesMap = new Map();
|
||||
let existingCustomModules = [];
|
||||
|
||||
if (await fs.pathExists(manifestPath)) {
|
||||
try {
|
||||
|
|
@ -402,6 +404,12 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) {
|
||||
// We filter here so manifest regeneration preserves source metadata only for custom modules that
|
||||
// are still installed. Without that, customModules can retain stale entries for modules that were removed.
|
||||
existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id));
|
||||
}
|
||||
} catch {
|
||||
// If we can't read existing manifest, continue with defaults
|
||||
}
|
||||
|
|
@ -437,6 +445,7 @@ class ManifestGenerator {
|
|||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
modules: updatedModules,
|
||||
customModules: existingCustomModules,
|
||||
ides: this.selectedIdes,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,111 @@ class CustomModules {
|
|||
|
||||
return this.paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble quick-update source candidates before install() hands them to discoverPaths().
|
||||
* This exists because discoverPaths() consumes already-prepared quick-update sources,
|
||||
* while quickUpdate() still has to build that source map from manifest, explicit inputs,
|
||||
* and cache conventions.
|
||||
* Precedence: manifest-backed paths, explicit sources override them, then cached modules.
|
||||
* @param {Object} config - Quick update configuration
|
||||
* @param {Object} existingInstall - Existing installation snapshot
|
||||
* @param {string} bmadDir - BMAD directory
|
||||
* @param {Object} externalModuleManager - External module manager
|
||||
* @returns {Promise<Map<string, Object>>} Map of custom module ID to source info
|
||||
*/
|
||||
async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) {
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
const customModuleSources = new Map();
|
||||
|
||||
if (existingInstall.customModules) {
|
||||
for (const customModule of existingInstall.customModules) {
|
||||
// Skip if no ID - can't reliably track or re-cache without it
|
||||
if (!customModule?.id) continue;
|
||||
|
||||
let sourcePath = customModule.sourcePath;
|
||||
if (sourcePath && sourcePath.startsWith('_config')) {
|
||||
// Paths are relative to BMAD dir, but we want absolute paths for install
|
||||
sourcePath = path.join(bmadDir, sourcePath);
|
||||
} else if (!sourcePath && customModule.relativePath) {
|
||||
// Fall back to relativePath
|
||||
sourcePath = path.resolve(projectRoot, customModule.relativePath);
|
||||
} else if (sourcePath && !path.isAbsolute(sourcePath)) {
|
||||
// If we have a sourcePath but it's not absolute, resolve it relative to project root
|
||||
sourcePath = path.resolve(projectRoot, sourcePath);
|
||||
}
|
||||
|
||||
// If we still don't have a valid source path, skip this module
|
||||
if (!sourcePath || !(await fs.pathExists(sourcePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
customModuleSources.set(customModule.id, {
|
||||
id: customModule.id,
|
||||
name: customModule.name || customModule.id,
|
||||
sourcePath,
|
||||
relativePath: customModule.relativePath,
|
||||
cached: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.customContent?.sources?.length > 0) {
|
||||
for (const source of config.customContent.sources) {
|
||||
if (source.id && source.path) {
|
||||
customModuleSources.set(source.id, {
|
||||
id: source.id,
|
||||
name: source.name || source.id,
|
||||
sourcePath: source.path,
|
||||
cached: false, // From CLI, will be re-cached
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (!(await fs.pathExists(cacheDir))) {
|
||||
return customModuleSources;
|
||||
}
|
||||
|
||||
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
for (const cachedModule of cachedModules) {
|
||||
const moduleId = cachedModule.name;
|
||||
const cachedPath = path.join(cacheDir, moduleId);
|
||||
|
||||
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
||||
if (!(await fs.pathExists(cachedPath))) {
|
||||
continue;
|
||||
}
|
||||
if (!cachedModule.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we already have this module from manifest
|
||||
if (customModuleSources.has(moduleId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is an external official module - skip cache for those
|
||||
const isExternal = await externalModuleManager.hasModule(moduleId);
|
||||
if (isExternal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is actually a custom module (has module.yaml)
|
||||
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
customModuleSources.set(moduleId, {
|
||||
id: moduleId,
|
||||
name: moduleId,
|
||||
sourcePath: cachedPath,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return customModuleSources;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CustomModules };
|
||||
|
|
|
|||
|
|
@ -50,12 +50,6 @@ export default defineConfig({
|
|||
defaultLocale: 'root',
|
||||
locales,
|
||||
|
||||
logo: {
|
||||
light: './public/img/bmad-light.png',
|
||||
dark: './public/img/bmad-dark.png',
|
||||
alt: 'BMAD Method',
|
||||
replacesTitle: true,
|
||||
},
|
||||
favicon: '/favicon.ico',
|
||||
|
||||
// Social links
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`;
|
|||
.ai-banner {
|
||||
width: 100%;
|
||||
height: var(--ai-banner-height, 2.75rem);
|
||||
background: #334155;
|
||||
color: #cbd5e1;
|
||||
background: #1a1a1a;
|
||||
color: #a1a1a1;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid rgba(140, 140, 255, 0.15);
|
||||
border-bottom: 1px solid #262626;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Truncate text on narrow screens */
|
||||
|
|
@ -32,15 +32,16 @@ const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`;
|
|||
max-width: 100%;
|
||||
}
|
||||
.ai-banner a {
|
||||
color: #B9B9FF;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ai-banner a:hover {
|
||||
color: #fafafa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ai-banner a:focus-visible {
|
||||
outline: 2px solid #B9B9FF;
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
/**
|
||||
* BMAD Method Documentation - Custom Styles for Starlight
|
||||
* Electric Blue theme optimized for dark mode
|
||||
* Dark theme matching bmadcode.com Ghost blog
|
||||
*
|
||||
* CSS Variable Mapping:
|
||||
* Docusaurus → Starlight
|
||||
* --ifm-color-primary → --sl-color-accent
|
||||
* --ifm-background-color → --sl-color-bg
|
||||
* --ifm-font-color-base → --sl-color-text
|
||||
* Design tokens from Ghost theme:
|
||||
* Background: #0a0a0a | Surface: #1a1a1a | Border: #262626
|
||||
* Accent: #3b82f6 | Gold: #d4a853 | Text: #fafafa/#a1a1a1/#666666
|
||||
*/
|
||||
|
||||
/* Google Fonts - match Ghost blog typography */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* ============================================
|
||||
COLOR PALETTE - Light Mode
|
||||
============================================ */
|
||||
|
|
@ -19,10 +20,10 @@
|
|||
/* Full-width content - override Starlight's default 45rem/67.5rem */
|
||||
--sl-content-width: 65rem;
|
||||
|
||||
/* Primary accent colors - purple to match Docusaurus */
|
||||
--sl-color-accent-low: #e0e0ff;
|
||||
--sl-color-accent: #5E5ED0;
|
||||
--sl-color-accent-high: #3333CC;
|
||||
/* Primary accent colors - blue to match Ghost blog */
|
||||
--sl-color-accent-low: #dbeafe;
|
||||
--sl-color-accent: #2563eb;
|
||||
--sl-color-accent-high: #1d4ed8;
|
||||
|
||||
/* Text colors */
|
||||
--sl-color-white: #1e293b;
|
||||
|
|
@ -35,13 +36,14 @@
|
|||
--sl-color-black: #f8fafc;
|
||||
|
||||
/* Font settings */
|
||||
--sl-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
--sl-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
Arial, sans-serif;
|
||||
--sl-font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
--sl-text-base: 1rem;
|
||||
--sl-line-height: 1.7;
|
||||
|
||||
/* Code highlighting */
|
||||
--sl-color-bg-inline-code: rgba(94, 94, 208, 0.1);
|
||||
--sl-color-bg-inline-code: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -51,35 +53,49 @@
|
|||
/* Full-width content - override Starlight's default */
|
||||
--sl-content-width: 65rem;
|
||||
|
||||
/* Primary accent colors - purple to match Docusaurus */
|
||||
--sl-color-accent-low: #2a2a5a;
|
||||
--sl-color-accent: #8C8CFF;
|
||||
--sl-color-accent-high: #B9B9FF;
|
||||
/* Primary accent colors - blue to match Ghost blog */
|
||||
--sl-color-accent-low: rgba(59, 130, 246, 0.12);
|
||||
--sl-color-accent: #3b82f6;
|
||||
--sl-color-accent-high: #60a5fa;
|
||||
|
||||
/* Background colors */
|
||||
--sl-color-bg: #1b1b1d;
|
||||
--sl-color-bg-nav: #1b1b1d;
|
||||
--sl-color-bg-sidebar: #1b1b1d;
|
||||
--sl-color-hairline-light: rgba(140, 140, 255, 0.1);
|
||||
--sl-color-hairline: rgba(140, 140, 255, 0.15);
|
||||
/* Background colors - match Ghost blog */
|
||||
--sl-color-bg: #0a0a0a;
|
||||
--sl-color-bg-nav: #0a0a0a;
|
||||
--sl-color-bg-sidebar: #0a0a0a;
|
||||
--sl-color-hairline-light: rgba(255, 255, 255, 0.06);
|
||||
--sl-color-hairline: #262626;
|
||||
|
||||
/* Text colors */
|
||||
--sl-color-white: #f8fafc;
|
||||
/* Text colors - match Ghost blog */
|
||||
--sl-color-white: #fafafa;
|
||||
--sl-color-gray-1: #e2e8f0;
|
||||
--sl-color-gray-2: #cbd5e1;
|
||||
--sl-color-gray-2: #a1a1a1;
|
||||
--sl-color-gray-3: #94a3b8;
|
||||
--sl-color-gray-4: #64748b;
|
||||
--sl-color-gray-4: #666666;
|
||||
--sl-color-gray-5: #475569;
|
||||
--sl-color-gray-6: #334155;
|
||||
--sl-color-black: #1b1b1d;
|
||||
--sl-color-gray-6: #262626;
|
||||
--sl-color-black: #0a0a0a;
|
||||
|
||||
/* Code highlighting */
|
||||
--sl-color-bg-inline-code: rgba(140, 140, 255, 0.15);
|
||||
--sl-color-bg-inline-code: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
/* Space Grotesk for all headings - match Ghost blog */
|
||||
.sl-markdown-content h1,
|
||||
.sl-markdown-content h2,
|
||||
.sl-markdown-content h3,
|
||||
.sl-markdown-content h4,
|
||||
.sl-markdown-content h5,
|
||||
.sl-markdown-content h6,
|
||||
.site-title,
|
||||
starlight-toc h2 {
|
||||
font-family: 'Space Grotesk', 'Inter', system-ui, sans-serif;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sl-markdown-content h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
|
@ -138,14 +154,14 @@
|
|||
|
||||
/* Active state - thin left accent bar */
|
||||
.sidebar-content a[aria-current='page'] {
|
||||
background-color: rgba(94, 94, 208, 0.08);
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
color: var(--sl-color-accent);
|
||||
border-left-color: var(--sl-color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .sidebar-content a[aria-current='page'] {
|
||||
background-color: rgba(140, 140, 255, 0.1);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
color: var(--sl-color-accent-high);
|
||||
border-left-color: var(--sl-color-accent);
|
||||
}
|
||||
|
|
@ -232,7 +248,8 @@ header.header .header.sl-flex {
|
|||
}
|
||||
|
||||
:root[data-theme='dark'] header.header {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid #262626;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
|
|
@ -281,20 +298,20 @@ header.header .header.sl-flex {
|
|||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--sl-color-accent);
|
||||
box-shadow: 0 8px 24px rgba(94, 94, 208, 0.15);
|
||||
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card {
|
||||
background: linear-gradient(145deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.8));
|
||||
border-color: rgba(140, 140, 255, 0.2);
|
||||
background: #1a1a1a;
|
||||
border-color: #262626;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card:hover {
|
||||
border-color: rgba(140, 140, 255, 0.5);
|
||||
border-color: #3b82f6;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(140, 140, 255, 0.2),
|
||||
0 0 0 1px rgba(140, 140, 255, 0.1);
|
||||
0 8px 32px rgba(59, 130, 246, 0.15),
|
||||
0 0 0 1px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Starlight card grid */
|
||||
|
|
@ -313,11 +330,11 @@ header.header .header.sl-flex {
|
|||
}
|
||||
|
||||
:root[data-theme='dark'] .sl-link-card {
|
||||
border-color: rgba(140, 140, 255, 0.2);
|
||||
border-color: #262626;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .sl-link-card:hover {
|
||||
border-color: rgba(140, 140, 255, 0.5);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
@ -372,21 +389,21 @@ table {
|
|||
}
|
||||
|
||||
:root[data-theme='dark'] table {
|
||||
border-color: rgba(140, 140, 255, 0.1);
|
||||
border-color: #262626;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] table th {
|
||||
background-color: rgba(140, 140, 255, 0.05);
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] table tr:nth-child(2n) {
|
||||
background-color: rgba(140, 140, 255, 0.02);
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
border-left-color: var(--sl-color-accent);
|
||||
background-color: rgba(94, 94, 208, 0.05);
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
|
@ -423,19 +440,19 @@ blockquote {
|
|||
|
||||
/* Note aside */
|
||||
.starlight-aside--note {
|
||||
background-color: rgba(94, 94, 208, 0.08);
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.starlight-aside--note .starlight-aside__title {
|
||||
color: #5C5CCC;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .starlight-aside--note {
|
||||
background-color: rgba(140, 140, 255, 0.12);
|
||||
background-color: rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .starlight-aside--note .starlight-aside__title {
|
||||
color: #8C8CFF;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Caution aside */
|
||||
|
|
@ -512,7 +529,7 @@ blockquote {
|
|||
ROADMAP STYLES
|
||||
============================================ */
|
||||
.roadmap-container {
|
||||
--color-planned: #6366f1;
|
||||
--color-planned: #3b82f6;
|
||||
--color-in-progress: #10b981;
|
||||
--color-exploring: #f59e0b;
|
||||
--color-bg-card: rgba(255, 255, 255, 0.03);
|
||||
|
|
@ -663,8 +680,8 @@ blockquote {
|
|||
}
|
||||
|
||||
.roadmap-badge.planned {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #6366f1;
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.roadmap-badge.exploring {
|
||||
|
|
@ -735,7 +752,7 @@ blockquote {
|
|||
.roadmap-future-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(245, 158, 11, 0.05));
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(212, 168, 83, 0.05));
|
||||
border: 1px solid var(--color-border);
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in New Issue