mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Merge pull request #822 from Tibsfox/fix/core-lifecycle-state
fix(core): correct phase lifecycle, milestone detection, state parsing, and CLI commit messages
This commit is contained in:
@@ -259,9 +259,13 @@ async function main() {
|
||||
|
||||
case 'commit': {
|
||||
const amend = args.includes('--amend');
|
||||
const message = args[1];
|
||||
// Parse --files flag (collect args after --files, stopping at other flags)
|
||||
const filesIndex = args.indexOf('--files');
|
||||
// Collect all positional args between command name and first flag,
|
||||
// then join them — handles both quoted ("multi word msg") and
|
||||
// unquoted (multi word msg) invocations from different shells
|
||||
const endIndex = filesIndex !== -1 ? filesIndex : args.length;
|
||||
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
||||
const message = messageArgs.join(' ') || undefined;
|
||||
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
||||
commands.cmdCommit(cwd, message, files, raw, amend);
|
||||
break;
|
||||
|
||||
@@ -392,7 +392,18 @@ function generateSlugInternal(text) {
|
||||
function getMilestoneInfo(cwd) {
|
||||
try {
|
||||
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
||||
// Strip <details>...</details> blocks so shipped milestones don't interfere
|
||||
|
||||
// First: check for list-format roadmaps using 🚧 (in-progress) marker
|
||||
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
||||
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
||||
if (inProgressMatch) {
|
||||
return {
|
||||
version: 'v' + inProgressMatch[1],
|
||||
name: inProgressMatch[2].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
||||
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
||||
// Extract version and name from the same ## heading for consistency
|
||||
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
||||
|
||||
@@ -783,7 +783,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
}
|
||||
}
|
||||
|
||||
// Find next phase
|
||||
// Find next phase — check both filesystem AND roadmap
|
||||
// Phases may be defined in ROADMAP.md but not yet scaffolded to disk,
|
||||
// so a filesystem-only scan would incorrectly report is_last_phase:true
|
||||
let nextPhaseNum = null;
|
||||
let nextPhaseName = null;
|
||||
let isLastPhase = true;
|
||||
@@ -809,6 +811,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
||||
// for phases that are defined but not yet planned (no directory on disk)
|
||||
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
||||
try {
|
||||
const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
||||
let pm;
|
||||
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
||||
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
||||
nextPhaseNum = pm[1];
|
||||
nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
|
||||
isLastPhase = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Update STATE.md
|
||||
if (fs.existsSync(statePath)) {
|
||||
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
@@ -65,11 +65,19 @@ function cmdStateGet(cwd, section, raw) {
|
||||
// Try to find markdown section or field
|
||||
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Check for **field:** value
|
||||
const fieldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||
const fieldMatch = content.match(fieldPattern);
|
||||
if (fieldMatch) {
|
||||
output({ [section]: fieldMatch[1].trim() }, raw, fieldMatch[1].trim());
|
||||
// Check for **field:** value (bold format)
|
||||
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) {
|
||||
output({ [section]: boldMatch[1].trim() }, raw, boldMatch[1].trim());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for field: value (plain format)
|
||||
const plainPattern = new RegExp(`^${fieldEscaped}:\\s*(.*)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
if (plainMatch) {
|
||||
output({ [section]: plainMatch[1].trim() }, raw, plainMatch[1].trim());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,10 +114,15 @@ function cmdStatePatch(cwd, patches, raw) {
|
||||
|
||||
for (const [field, value] of Object.entries(patches)) {
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
|
||||
if (boldPattern.test(content)) {
|
||||
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
results.updated.push(field);
|
||||
} else if (plainPattern.test(content)) {
|
||||
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
results.updated.push(field);
|
||||
} else {
|
||||
results.failed.push(field);
|
||||
@@ -135,9 +148,15 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
try {
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
if (boldPattern.test(content)) {
|
||||
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true });
|
||||
} else if (plainPattern.test(content)) {
|
||||
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true });
|
||||
} else {
|
||||
@@ -152,16 +171,26 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
|
||||
function stateExtractField(content, fieldName) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
||||
const match = content.match(pattern);
|
||||
return match ? match[1].trim() : null;
|
||||
// Try **Field:** bold format first
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) return boldMatch[1].trim();
|
||||
// Fall back to plain Field: format
|
||||
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
function stateReplaceField(content, fieldName, newValue) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (pattern.test(content)) {
|
||||
return content.replace(pattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
||||
if (plainPattern.test(content)) {
|
||||
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -256,9 +285,15 @@ function cmdStateUpdateProgress(cwd, raw) {
|
||||
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
||||
const progressStr = `[${bar}] ${percent}%`;
|
||||
|
||||
const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
||||
if (progressPattern.test(content)) {
|
||||
content = content.replace(progressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
// Try **Progress:** bold format first, then plain Progress: format
|
||||
const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
||||
const plainProgressPattern = /^(Progress:\s*).*/im;
|
||||
if (boldProgressPattern.test(content)) {
|
||||
content = content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
||||
} else if (plainProgressPattern.test(content)) {
|
||||
content = content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
||||
} else {
|
||||
@@ -414,11 +449,17 @@ function cmdStateSnapshot(cwd, raw) {
|
||||
|
||||
const content = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
// Helper to extract **Field:** value patterns
|
||||
// Helper to extract field values — supports both **Field:** bold format
|
||||
// and plain Field: format (STATE.md may use either depending on version)
|
||||
const extractField = (fieldName) => {
|
||||
const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
||||
const match = content.match(pattern);
|
||||
return match ? match[1].trim() : null;
|
||||
// Try **Field:** format first (bold markdown)
|
||||
const boldPattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) return boldMatch[1].trim();
|
||||
// Fall back to plain Field: format
|
||||
const plainPattern = new RegExp(`^${fieldName}:\\s*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
};
|
||||
|
||||
// Extract basic fields
|
||||
@@ -477,9 +518,12 @@ function cmdStateSnapshot(cwd, raw) {
|
||||
const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
if (sessionMatch) {
|
||||
const sessionSection = sessionMatch[1];
|
||||
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i);
|
||||
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i);
|
||||
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i);
|
||||
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Last Date:\s*(.+)/im);
|
||||
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Stopped At:\s*(.+)/im);
|
||||
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Resume File:\s*(.+)/im);
|
||||
|
||||
if (lastDateMatch) session.last_date = lastDateMatch[1].trim();
|
||||
if (stoppedAtMatch) session.stopped_at = stoppedAtMatch[1].trim();
|
||||
@@ -513,10 +557,14 @@ function cmdStateSnapshot(cwd, raw) {
|
||||
* reliably via `state json` instead of fragile regex parsing.
|
||||
*/
|
||||
function buildStateFrontmatter(bodyContent, cwd) {
|
||||
// Supports both **Field:** bold and plain Field: format (see state-snapshot)
|
||||
const extractField = (fieldName) => {
|
||||
const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
||||
const match = bodyContent.match(pattern);
|
||||
return match ? match[1].trim() : null;
|
||||
const boldPattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
||||
const boldMatch = bodyContent.match(boldPattern);
|
||||
if (boldMatch) return boldMatch[1].trim();
|
||||
const plainPattern = new RegExp(`^${fieldName}:\\s*(.+)`, 'im');
|
||||
const plainMatch = bodyContent.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
};
|
||||
|
||||
const currentPhase = extractField('Current Phase');
|
||||
|
||||
Reference in New Issue
Block a user