Compare commits
3 Commits
c2bfd91a1c
...
f41a1549d4
| Author | SHA1 | Date |
|---|---|---|
|
|
f41a1549d4 | |
|
|
49f180d58e | |
|
|
560a2e3a6f |
|
|
@ -350,6 +350,7 @@ Amelia (Developer): "I found our retrospectives from Epic {{prev_epic_num}}. Let
|
||||||
|
|
||||||
**Action Item Follow-Through:**
|
**Action Item Follow-Through:**
|
||||||
- For each action item from Epic {{prev_epic_num}} retro, check if it was completed
|
- For each action item from Epic {{prev_epic_num}} retro, check if it was completed
|
||||||
|
- Cross-check the action_items section in {sprint_status_file} (if present) for Epic {{prev_epic_num}} entries and their current status
|
||||||
- Look for evidence in current epic's story records
|
- Look for evidence in current epic's story records
|
||||||
- Mark each action item: ✅ Completed, ⏳ In Progress, ❌ Not Addressed
|
- Mark each action item: ✅ Completed, ⏳ In Progress, ❌ Not Addressed
|
||||||
|
|
||||||
|
|
@ -1403,6 +1404,19 @@ Amelia (Developer): "See you all when prep work is done. Meeting adjourned!"
|
||||||
<action>Find development_status key "epic-{{epic_number}}-retrospective"</action>
|
<action>Find development_status key "epic-{{epic_number}}-retrospective"</action>
|
||||||
<action>Verify current status (typically "optional" or "pending")</action>
|
<action>Verify current status (typically "optional" or "pending")</action>
|
||||||
<action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action>
|
<action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action>
|
||||||
|
<action>Append each Epic {{epic_number}} action item to the action_items section, creating the section after development_status if missing. One entry per item:</action>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action_items:
|
||||||
|
- epic: {{epic_number}}
|
||||||
|
action: "{{action_description}}"
|
||||||
|
owner: "{{owner}}"
|
||||||
|
status: open
|
||||||
|
```
|
||||||
|
|
||||||
|
<action>Quote action and owner values so punctuation (e.g., "#") cannot break YAML parsing</action>
|
||||||
|
|
||||||
|
<action>Update Epic {{prev_epic_num}} action_items entries based on Step 4 follow-through: ✅ Completed → done, ⏳ In Progress → in-progress, ❌ Not Addressed → keep existing status (do not modify)</action>
|
||||||
<action>Update last_updated field to current date</action>
|
<action>Update last_updated field to current date</action>
|
||||||
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
|
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
|
||||||
|
|
||||||
|
|
@ -1412,6 +1426,7 @@ Amelia (Developer): "See you all when prep work is done. Meeting adjourned!"
|
||||||
|
|
||||||
Retrospective key: epic-{{epic_number}}-retrospective
|
Retrospective key: epic-{{epic_number}}-retrospective
|
||||||
Status: {{previous_status}} → done
|
Status: {{previous_status}} → done
|
||||||
|
Action items recorded: {{action_count}}
|
||||||
</output>
|
</output>
|
||||||
</check>
|
</check>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ development_status:
|
||||||
|
|
||||||
- If existing `{status_file}` exists and has more advanced status, preserve it
|
- If existing `{status_file}` exists and has more advanced status, preserve it
|
||||||
- Never downgrade status (e.g., don't change `done` to `ready-for-dev`)
|
- Never downgrade status (e.g., don't change `done` to `ready-for-dev`)
|
||||||
|
- If existing `{status_file}` has an `action_items` section, carry it over unchanged
|
||||||
|
|
||||||
**Status Flow Reference:**
|
**Status Flow Reference:**
|
||||||
|
|
||||||
|
|
@ -194,12 +195,18 @@ development_status:
|
||||||
# - optional: Can be completed but not required
|
# - optional: Can be completed but not required
|
||||||
# - done: Retrospective has been completed
|
# - done: Retrospective has been completed
|
||||||
#
|
#
|
||||||
|
# Action Item Status:
|
||||||
|
# - open: Committed during a retrospective, not yet addressed
|
||||||
|
# - in-progress: Actively being worked on
|
||||||
|
# - done: Completed
|
||||||
|
#
|
||||||
# WORKFLOW NOTES:
|
# WORKFLOW NOTES:
|
||||||
# ===============
|
# ===============
|
||||||
# - Epic transitions to 'in-progress' automatically when first story is created
|
# - Epic transitions to 'in-progress' automatically when first story is created
|
||||||
# - Stories can be worked in parallel if team capacity allows
|
# - Stories can be worked in parallel if team capacity allows
|
||||||
# - Developer typically creates next story after previous one is 'done' to incorporate learnings
|
# - Developer typically creates next story after previous one is 'done' to incorporate learnings
|
||||||
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
|
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
|
||||||
|
# - Retrospective appends its action items to action_items; sprint-status surfaces open ones
|
||||||
|
|
||||||
generated: { date }
|
generated: { date }
|
||||||
last_updated: { date }
|
last_updated: { date }
|
||||||
|
|
@ -215,6 +222,7 @@ development_status:
|
||||||
<action>Write the complete sprint status YAML to {status_file}</action>
|
<action>Write the complete sprint status YAML to {status_file}</action>
|
||||||
<action>CRITICAL: Metadata appears TWICE - once as comments (#) for documentation, once as YAML key:value fields for parsing</action>
|
<action>CRITICAL: Metadata appears TWICE - once as comments (#) for documentation, once as YAML key:value fields for parsing</action>
|
||||||
<action>Ensure all items are ordered: epic, its stories, its retrospective, next epic...</action>
|
<action>Ensure all items are ordered: epic, its stories, its retrospective, next epic...</action>
|
||||||
|
<action>If the existing file had an action_items section, write it back unchanged after development_status</action>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step n="5" goal="Validate and report">
|
<step n="5" goal="Validate and report">
|
||||||
|
|
@ -223,7 +231,8 @@ development_status:
|
||||||
- [ ] Every epic in epic files appears in {status_file}
|
- [ ] Every epic in epic files appears in {status_file}
|
||||||
- [ ] Every story in epic files appears in {status_file}
|
- [ ] Every story in epic files appears in {status_file}
|
||||||
- [ ] Every epic has a corresponding retrospective entry
|
- [ ] Every epic has a corresponding retrospective entry
|
||||||
- [ ] No items in {status_file} that don't exist in epic files
|
- [ ] No development_status items in {status_file} that don't exist in epic files
|
||||||
|
- [ ] action_items section (if it existed) carried over unchanged
|
||||||
- [ ] All status values are legal (match state machine definitions)
|
- [ ] All status values are legal (match state machine definitions)
|
||||||
- [ ] File is valid YAML syntax
|
- [ ] File is valid YAML syntax
|
||||||
|
|
||||||
|
|
@ -291,6 +300,16 @@ optional ↔ done
|
||||||
- **optional**: Ready to be conducted but not required
|
- **optional**: Ready to be conducted but not required
|
||||||
- **done**: Finished
|
- **done**: Finished
|
||||||
|
|
||||||
|
**Action Item Status:**
|
||||||
|
|
||||||
|
```
|
||||||
|
open → in-progress → done
|
||||||
|
```
|
||||||
|
|
||||||
|
- **open**: Committed during a retrospective, not yet addressed
|
||||||
|
- **in-progress**: Actively being worked on
|
||||||
|
- **done**: Completed
|
||||||
|
|
||||||
### Guidelines
|
### Guidelines
|
||||||
|
|
||||||
1. **Epic Activation**: Mark epic as `in-progress` when starting work on its first story
|
1. **Epic Activation**: Mark epic as `in-progress` when starting work on its first story
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
- [ ] Every epic found in epic\*.md files appears in sprint-status.yaml
|
- [ ] Every epic found in epic\*.md files appears in sprint-status.yaml
|
||||||
- [ ] Every story found in epic\*.md files appears in sprint-status.yaml
|
- [ ] Every story found in epic\*.md files appears in sprint-status.yaml
|
||||||
- [ ] Every epic has a corresponding retrospective entry
|
- [ ] Every epic has a corresponding retrospective entry
|
||||||
- [ ] No items in sprint-status.yaml that don't exist in epic files
|
- [ ] No development_status items in sprint-status.yaml that don't exist in epic files
|
||||||
|
- [ ] action_items section (if it existed) carried over unchanged
|
||||||
|
|
||||||
### Parsing Verification
|
### Parsing Verification
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,17 @@
|
||||||
# - optional: Can be completed but not required
|
# - optional: Can be completed but not required
|
||||||
# - done: Retrospective has been completed
|
# - done: Retrospective has been completed
|
||||||
#
|
#
|
||||||
|
# Action Item Status:
|
||||||
|
# - open: Committed during a retrospective, not yet addressed
|
||||||
|
# - in-progress: Actively being worked on
|
||||||
|
# - done: Completed
|
||||||
|
#
|
||||||
# WORKFLOW NOTES:
|
# WORKFLOW NOTES:
|
||||||
# ===============
|
# ===============
|
||||||
# - Mark epic as 'in-progress' when starting work on its first story
|
# - Mark epic as 'in-progress' when starting work on its first story
|
||||||
# - Developer typically creates next story ONLY after previous one is 'done' to incorporate learnings
|
# - Developer typically creates next story ONLY after previous one is 'done' to incorporate learnings
|
||||||
# - Dev moves story to 'review', then Dev runs code-review (fresh context, ideally different LLM)
|
# - Dev moves story to 'review', then Dev runs code-review (fresh context, ideally different LLM)
|
||||||
|
# - Retrospective appends its action items to action_items; sprint-status surfaces open ones
|
||||||
|
|
||||||
# EXAMPLE STRUCTURE (your actual epics/stories will replace these):
|
# EXAMPLE STRUCTURE (your actual epics/stories will replace these):
|
||||||
|
|
||||||
|
|
@ -54,3 +60,10 @@ development_status:
|
||||||
2-2-chat-interface: backlog
|
2-2-chat-interface: backlog
|
||||||
2-3-llm-integration: backlog
|
2-3-llm-integration: backlog
|
||||||
epic-2-retrospective: optional
|
epic-2-retrospective: optional
|
||||||
|
|
||||||
|
# Action items committed during retrospectives (section created by the retrospective workflow)
|
||||||
|
action_items:
|
||||||
|
- epic: 1
|
||||||
|
action: "Add error-handling review to the code review checklist"
|
||||||
|
owner: "Charlie"
|
||||||
|
status: open
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,14 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
|
||||||
<action>Map legacy epic status "contexted" → "in-progress"</action>
|
<action>Map legacy epic status "contexted" → "in-progress"</action>
|
||||||
<action>Count epic statuses: backlog, in-progress, done</action>
|
<action>Count epic statuses: backlog, in-progress, done</action>
|
||||||
<action>Count retrospective statuses: optional, done</action>
|
<action>Count retrospective statuses: optional, done</action>
|
||||||
|
<action>Parse action_items list if present. Set open_action_items = entries with status "open" or "in-progress"</action>
|
||||||
|
|
||||||
<action>Validate all statuses against known values:</action>
|
<action>Validate all statuses against known values:</action>
|
||||||
|
|
||||||
- Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy)
|
- Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy)
|
||||||
- Valid epic statuses: backlog, in-progress, done, contexted (legacy)
|
- Valid epic statuses: backlog, in-progress, done, contexted (legacy)
|
||||||
- Valid retrospective statuses: optional, done
|
- Valid retrospective statuses: optional, done
|
||||||
|
- Valid action item statuses: open, in-progress, done
|
||||||
|
|
||||||
<check if="any status is unrecognized">
|
<check if="any status is unrecognized">
|
||||||
<output>
|
<output>
|
||||||
|
|
@ -132,6 +134,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
|
||||||
- Stories: backlog, ready-for-dev, in-progress, review, done
|
- Stories: backlog, ready-for-dev, in-progress, review, done
|
||||||
- Epics: backlog, in-progress, done
|
- Epics: backlog, in-progress, done
|
||||||
- Retrospectives: optional, done
|
- Retrospectives: optional, done
|
||||||
|
- Action items: open, in-progress, done
|
||||||
</output>
|
</output>
|
||||||
<ask>How should these be corrected?
|
<ask>How should these be corrected?
|
||||||
{{#each invalid_entries}}
|
{{#each invalid_entries}}
|
||||||
|
|
@ -181,6 +184,14 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
||||||
|
|
||||||
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
||||||
|
|
||||||
|
{{#if open_action_items}}
|
||||||
|
**Open Action Items:**
|
||||||
|
{{#each open_action_items}}
|
||||||
|
|
||||||
|
- {{action}} — {{status}} (epic {{epic}}, owner: {{owner}})
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if risks}}
|
{{#if risks}}
|
||||||
**Risks:**
|
**Risks:**
|
||||||
{{#each risks}}
|
{{#each risks}}
|
||||||
|
|
@ -243,6 +254,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
||||||
<template-output>epic_backlog = {{epic_backlog}}</template-output>
|
<template-output>epic_backlog = {{epic_backlog}}</template-output>
|
||||||
<template-output>epic_in_progress = {{epic_in_progress}}</template-output>
|
<template-output>epic_in_progress = {{epic_in_progress}}</template-output>
|
||||||
<template-output>epic_done = {{epic_done}}</template-output>
|
<template-output>epic_done = {{epic_done}}</template-output>
|
||||||
|
<template-output>open_action_items = {{open_action_items}}</template-output>
|
||||||
<template-output>risks = {{risks}}</template-output>
|
<template-output>risks = {{risks}}</template-output>
|
||||||
<action>Return to caller</action>
|
<action>Return to caller</action>
|
||||||
</step>
|
</step>
|
||||||
|
|
@ -283,6 +295,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
||||||
- Stories: backlog, ready-for-dev, in-progress, review, done (legacy: drafted)
|
- Stories: backlog, ready-for-dev, in-progress, review, done (legacy: drafted)
|
||||||
- Epics: backlog, in-progress, done (legacy: contexted)
|
- Epics: backlog, in-progress, done (legacy: contexted)
|
||||||
- Retrospectives: optional, done
|
- Retrospectives: optional, done
|
||||||
|
- Action items (if present): open, in-progress, done
|
||||||
<check if="any invalid status found">
|
<check if="any invalid status found">
|
||||||
<template-output>is_valid = false</template-output>
|
<template-output>is_valid = false</template-output>
|
||||||
<template-output>error = "Invalid status values: {{invalid_entries}}"</template-output>
|
<template-output>error = "Invalid status values: {{invalid_entries}}"</template-output>
|
||||||
|
|
|
||||||
|
|
@ -3318,6 +3318,144 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 46: Python environment check (version parsing + classification)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 46: python-check version parsing and classification${colors.reset}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-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');
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 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, 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');
|
||||||
|
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;
|
||||||
|
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),
|
||||||
|
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: 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');
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`);
|
||||||
|
console.log(error.stack);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
@ -161,6 +161,16 @@ class UI {
|
||||||
const messageLoader = new MessageLoader();
|
const messageLoader = new MessageLoader();
|
||||||
await messageLoader.displayStartMessage();
|
await messageLoader.displayStartMessage();
|
||||||
|
|
||||||
|
// Probe the local Python before any other prompts: several BMAD features
|
||||||
|
// (memlog session memory, TOML config resolution) need Python 3.11+ at
|
||||||
|
// runtime. Warn-don't-block, but require an explicit ack so the warning
|
||||||
|
// can't scroll past unseen. The installer runs in the destination
|
||||||
|
// environment, so probing PATH here tests the right machine.
|
||||||
|
// Skip the ack when stdin isn't a TTY (CI/Docker/piped): clack's select
|
||||||
|
// on closed stdin resolves to cancel, which would silently exit 0.
|
||||||
|
const { checkPythonEnvironment } = require('./core/python-check');
|
||||||
|
await checkPythonEnvironment({ nonInteractive: !!options.yes || !process.stdin.isTTY });
|
||||||
|
|
||||||
// Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
|
// Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
|
||||||
// are surfaced immediately so the user sees them before any git ops run.
|
// are surfaced immediately so the user sees them before any git ops run.
|
||||||
const channelOptions = parseChannelOptions(options);
|
const channelOptions = parseChannelOptions(options);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue