* fix(sdk): forward --ws workstream flag through query dispatch (closes #2524) - cli.ts: pass args.ws as workstream to registry.dispatch() - registry.ts: add workstream? param to dispatch(), thread to handler - utils.ts: add optional workstream? to QueryHandler type signature - helpers.ts: planningPaths() accepts workstream? and uses relPlanningPath() - All ~26 query handlers updated to receive and pass workstream to planningPaths() - Config/commit/intel handlers use _workstream (project-global, not scoped) - Add failing-then-passing test: tests/bug-2524-sdk-query-ws-flag.test.cjs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): forward workstream to all downstream query helpers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): rewrite #2524 test as static source assertions — no sdk/dist build in CI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
/**
|
||
* roadmap.update-plan-progress — sync ROADMAP.md progress table + plan checkboxes
|
||
* from on-disk PLAN/SUMMARY counts for a phase.
|
||
*
|
||
* Port of `cmdRoadmapUpdatePlanProgress` from get-shit-done/bin/lib/roadmap.cjs
|
||
* (lines 257–354). Uses `findPhase` for disk stats and `readModifyWriteRoadmapMd`
|
||
* for atomic writes (same pattern as `phase.complete`).
|
||
*/
|
||
|
||
import { findPhase } from './phase.js';
|
||
import { readModifyWriteRoadmapMd, replaceInCurrentMilestone } from './phase-lifecycle.js';
|
||
import { existsSync } from 'node:fs';
|
||
import { escapeRegex, planningPaths } from './helpers.js';
|
||
import { GSDError, ErrorClassification } from '../errors.js';
|
||
import type { QueryHandler } from './utils.js';
|
||
|
||
export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir, workstream) => {
|
||
const phaseNum = args[0];
|
||
if (!phaseNum) {
|
||
throw new GSDError('phase number required for roadmap update-plan-progress', ErrorClassification.Validation);
|
||
}
|
||
|
||
const phaseResult = await findPhase([phaseNum], projectDir, workstream);
|
||
const info = phaseResult.data as {
|
||
found: boolean;
|
||
plans: string[];
|
||
summaries: string[];
|
||
};
|
||
|
||
if (!info.found) {
|
||
throw new GSDError(`Phase ${phaseNum} not found`, ErrorClassification.Validation);
|
||
}
|
||
|
||
const planCount = info.plans.length;
|
||
const summaryCount = info.summaries.length;
|
||
|
||
if (planCount === 0) {
|
||
return {
|
||
data: {
|
||
updated: false,
|
||
reason: 'No plans found',
|
||
plan_count: 0,
|
||
summary_count: 0,
|
||
},
|
||
};
|
||
}
|
||
|
||
const isComplete = summaryCount >= planCount;
|
||
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
||
const today = new Date().toISOString().split('T')[0]!;
|
||
|
||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||
if (!existsSync(roadmapPath)) {
|
||
return {
|
||
data: {
|
||
updated: false,
|
||
reason: 'ROADMAP.md not found',
|
||
plan_count: planCount,
|
||
summary_count: summaryCount,
|
||
},
|
||
};
|
||
}
|
||
|
||
await readModifyWriteRoadmapMd(projectDir, (roadmapContent) => {
|
||
const phaseEscaped = escapeRegex(phaseNum);
|
||
|
||
const tableRowPattern = new RegExp(
|
||
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
||
'im',
|
||
);
|
||
const dateField = isComplete ? ` ${today} ` : ' ';
|
||
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
||
const cells = fullRow.split('|').slice(1, -1);
|
||
if (cells.length === 5) {
|
||
cells[2] = ` ${summaryCount}/${planCount} `;
|
||
cells[3] = ` ${status.padEnd(11)}`;
|
||
cells[4] = dateField;
|
||
} else if (cells.length === 4) {
|
||
cells[1] = ` ${summaryCount}/${planCount} `;
|
||
cells[2] = ` ${status.padEnd(11)}`;
|
||
cells[3] = dateField;
|
||
}
|
||
return '|' + cells.join('|') + '|';
|
||
});
|
||
|
||
const planCountPattern = new RegExp(
|
||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||
'i',
|
||
);
|
||
const planCountText = isComplete
|
||
? `${summaryCount}/${planCount} plans complete`
|
||
: `${summaryCount}/${planCount} plans executed`;
|
||
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
||
|
||
if (isComplete) {
|
||
const checkboxPattern = new RegExp(
|
||
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
|
||
'i',
|
||
);
|
||
roadmapContent = replaceInCurrentMilestone(
|
||
roadmapContent,
|
||
checkboxPattern,
|
||
`$1x$2 (completed ${today})`,
|
||
);
|
||
}
|
||
|
||
const summaries = info.summaries;
|
||
for (const summaryFile of summaries) {
|
||
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||
if (!planId) continue;
|
||
const planEscaped = escapeRegex(planId);
|
||
const planCheckboxPattern = new RegExp(
|
||
`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
|
||
'i',
|
||
);
|
||
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
||
}
|
||
|
||
return roadmapContent;
|
||
}, workstream);
|
||
|
||
return {
|
||
data: {
|
||
updated: true,
|
||
phase: phaseNum,
|
||
plan_count: planCount,
|
||
summary_count: summaryCount,
|
||
status,
|
||
complete: isComplete,
|
||
},
|
||
};
|
||
};
|