cmte
Version:
Design by Committee™ except it's just you and LLMs
154 lines (133 loc) • 4.34 kB
JavaScript
import path from 'path';
/**
* Components that make up a Committee path
*/
/**
* Utility class for handling Committee paths
*/
export class CommitteePaths {
/**
* Constructs a path from components
*/
static constructPath(base, components) {
const parts = [];
// Add phase
parts.push(components.phase);
// Add phase iteration if present
if (components.phaseIteration) {
parts.push(components.phaseIteration);
}
// Add set
if (components.set) {
parts.push(components.set);
// Add set iteration if present
if (components.setIteration) {
parts.push(components.setIteration);
}
}
// Add task if present
if (components.task) {
parts.push(components.task);
// Add task iteration if present
if (components.taskIteration) {
parts.push(components.taskIteration);
}
}
// Filter out any empty or undefined parts
const filteredParts = parts.filter(part => part && part.trim());
return path.join(base, ...filteredParts);
}
/**
* Resolves a full path back into its components
*/
static resolvePath(fullPath) {
// Remove file extension if present
const withoutExt = fullPath.replace(/\.[^/.]+$/, '');
// Split path into parts
const parts = withoutExt.split(path.sep).filter(Boolean);
// We expect paths to be in the format:
// phase/phase-iteration?/set/set-iteration?/task?/task-iteration?
const components = {
phase: parts[0],
set: parts[parts.length >= 2 ? 1 : 0]
};
// If we have more than 2 parts, we need to determine what they are
if (parts.length > 2) {
let currentIndex = 2;
// Check if we have a phase iteration
if (parts[1].startsWith(parts[0]) || !parts[1].includes('/')) {
components.phaseIteration = parts[1];
components.set = parts[2];
currentIndex = 3;
}
// Check for set iteration
if (currentIndex < parts.length) {
if (parts[currentIndex].startsWith(components.set) || !parts[currentIndex].includes('/')) {
components.setIteration = parts[currentIndex];
currentIndex++;
}
}
// Check for task and task iteration
if (currentIndex < parts.length) {
components.task = parts[parts.length - 1];
if (currentIndex < parts.length - 1) {
components.taskIteration = parts[parts.length - 2];
}
}
}
return components;
}
/**
* Gets a name for an iteration based on the item
*/
static getIterationName(item, index, prefix) {
// Sanitize the prefix to be filesystem safe
const safePrefix = prefix.replace(/[^a-zA-Z0-9-_]/g, '-');
if (typeof item === 'string') {
// Sanitize string values to be filesystem safe
return item.replace(/[^a-zA-Z0-9-_]/g, '-');
}
if (item?.name) {
return String(item.name).replace(/[^a-zA-Z0-9-_]/g, '-');
}
if (item?.id) {
return String(item.id).replace(/[^a-zA-Z0-9-_]/g, '-');
}
return `${safePrefix}-${index + 1}`;
}
/**
* Converts a camelCase string to kebab-case
*/
static camelToKebab(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
/**
* Constructs an output file path
*/
static constructOutputPath(base, components, type) {
// For task outputs, save directly in the phase directory
if (components.task) {
const basePath = path.join(base, components.phase);
const outputName = this.camelToKebab(components.outputName || components.task);
if (type) {
return path.join(basePath, `${outputName}-${type}.o.md`);
}
return path.join(basePath, `${outputName}.o.md`);
}
// For set and phase outputs, use the full path structure
const basePath = this.constructPath(base, components);
const filename = components.set;
if (type) {
return path.join(basePath, `${filename}-${type}.o.md`);
}
return path.join(basePath, `${filename}.o.md`);
}
/**
* Constructs a prompt file path
*/
static constructPromptPath(base, components, type) {
const basePath = this.constructPath(base, components);
const filename = components.task || components.set;
return path.join(basePath, `${filename}-${type}.prompt.xml`);
}
}