installer and bundler progress

This commit is contained in:
Brian Madison
2025-09-30 00:19:56 -05:00
parent ae136ceb03
commit c26220daec
15 changed files with 278 additions and 60 deletions

View File

@@ -173,12 +173,17 @@ class WebBundler {
// Resolve dependencies with warning tracking
const dependencyWarnings = [];
const dependencies = await this.resolveAgentDependencies(agentXml, moduleName, dependencyWarnings);
const { dependencies, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, dependencyWarnings);
if (dependencyWarnings.length > 0) {
this.stats.warnings.push({ agent: agentName, warnings: dependencyWarnings });
}
// Remove commands for skipped workflows from agent XML
if (skippedWorkflows.length > 0) {
agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows);
}
// Build the bundle (no manifests for individual agents)
const bundle = this.buildAgentBundle(agentXml, dependencies);
@@ -266,6 +271,7 @@ class WebBundler {
async resolveAgentDependencies(agentXml, moduleName, warnings = []) {
const dependencies = new Map();
const processed = new Set();
const skippedWorkflows = [];
// Extract file references from agent XML
const { refs, workflowRefs } = this.extractFileReferences(agentXml);
@@ -277,10 +283,13 @@ class WebBundler {
// Process workflow references with special handling
for (const workflowRef of workflowRefs) {
await this.processWorkflowDependency(workflowRef, dependencies, processed, moduleName, warnings);
const result = await this.processWorkflowDependency(workflowRef, dependencies, processed, moduleName, warnings);
if (result && result.skipped) {
skippedWorkflows.push(workflowRef);
}
}
return dependencies;
return { dependencies, skippedWorkflows };
}
/**
@@ -330,6 +339,27 @@ class WebBundler {
return { refs: [...refs], workflowRefs: [...workflowRefs] };
}
/**
* Remove commands from agent XML that reference skipped workflows
*/
removeSkippedWorkflowCommands(agentXml, skippedWorkflows) {
let modifiedXml = agentXml;
// For each skipped workflow, find and remove the corresponding <c> command
for (const workflowPath of skippedWorkflows) {
// Match: <c cmd="..." run-workflow="workflowPath">...</c>
// Need to escape special regex characters in the path
const escapedPath = workflowPath.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
// Pattern to match the command line with this workflow
const pattern = new RegExp(`\\s*<c\\s+cmd="[^"]*"\\s+run-workflow="[^"]*${escapedPath}"[^>]*>.*?</c>\\s*`, 'gs');
modifiedXml = modifiedXml.replace(pattern, '');
}
return modifiedXml;
}
/**
* Process a file dependency recursively
*/
@@ -504,7 +534,7 @@ class WebBundler {
async processWorkflowDependency(workflowPath, dependencies, processed, moduleName, warnings = []) {
// Skip if already processed
if (processed.has(workflowPath)) {
return;
return { skipped: false };
}
processed.add(workflowPath);
@@ -513,7 +543,7 @@ class WebBundler {
if (!actualPath || !(await fs.pathExists(actualPath))) {
warnings.push(workflowPath);
return;
return { skipped: true };
}
// Read and parse YAML file
@@ -524,12 +554,28 @@ class WebBundler {
workflowConfig = yaml.load(yamlContent);
} catch (error) {
warnings.push(`${workflowPath} (invalid YAML: ${error.message})`);
return;
return { skipped: true };
}
// Include the YAML file itself, wrapped in XML
// Check if web_bundle is explicitly set to false
if (workflowConfig.web_bundle === false) {
// Mark this workflow as skipped so we can remove the command from agent
return { skipped: true, workflowPath };
}
// Create YAML content with only web_bundle section (flattened)
let bundleYamlContent;
if (workflowConfig.web_bundle && typeof workflowConfig.web_bundle === 'object') {
// Only include the web_bundle content, flattened to root level
bundleYamlContent = yaml.dump(workflowConfig.web_bundle);
} else {
// If no web_bundle section, include full YAML
bundleYamlContent = yamlContent;
}
// Include the YAML file with only web_bundle content, wrapped in XML
const yamlId = workflowPath.replace(/^{project-root}\//, '');
const wrappedYaml = this.wrapContentInXml(yamlContent, yamlId, 'yaml');
const wrappedYaml = this.wrapContentInXml(bundleYamlContent, yamlId, 'yaml');
dependencies.set(yamlId, wrappedYaml);
// Always include core workflow task when processing workflows
@@ -566,6 +612,8 @@ class WebBundler {
dependencies.set(bundleFilePath, wrappedContent);
}
}
return { skipped: false };
}
/**

View File

@@ -271,9 +271,15 @@ class ModuleManager {
}
}
// Copy the file
await fs.ensureDir(path.dirname(targetFile));
await fs.copy(sourceFile, targetFile, { overwrite: true });
// Check if this is a workflow.yaml file
if (file.endsWith('workflow.yaml')) {
await fs.ensureDir(path.dirname(targetFile));
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
} else {
// Copy the file normally
await fs.ensureDir(path.dirname(targetFile));
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
// Track the file if callback provided
if (fileTrackingCallback) {
@@ -282,6 +288,91 @@ class ModuleManager {
}
}
/**
* Copy workflow.yaml file with web_bundle section stripped
* Preserves comments, formatting, and line breaks
* @param {string} sourceFile - Source workflow.yaml file path
* @param {string} targetFile - Target workflow.yaml file path
*/
async copyWorkflowYamlStripped(sourceFile, targetFile) {
// Read the source YAML file
let yamlContent = await fs.readFile(sourceFile, 'utf8');
try {
// First check if web_bundle exists by parsing
const workflowConfig = yaml.load(yamlContent);
if (workflowConfig.web_bundle === undefined) {
// No web_bundle section, just copy as-is
await fs.writeFile(targetFile, yamlContent, 'utf8');
return;
}
// Remove web_bundle section using regex to preserve formatting
// Match the web_bundle key and all its content (including nested items)
// This handles both web_bundle: false and web_bundle: {...}
// Find the line that starts web_bundle
const lines = yamlContent.split('\n');
let startIdx = -1;
let endIdx = -1;
let baseIndent = 0;
// Find the start of web_bundle section
for (const [i, line] of lines.entries()) {
const match = line.match(/^(\s*)web_bundle:/);
if (match) {
startIdx = i;
baseIndent = match[1].length;
break;
}
}
if (startIdx === -1) {
// web_bundle not found in text (shouldn't happen), copy as-is
await fs.writeFile(targetFile, yamlContent, 'utf8');
return;
}
// Find the end of web_bundle section
// It ends when we find a line with same or less indentation that's not empty/comment
endIdx = startIdx;
for (let i = startIdx + 1; i < lines.length; i++) {
const line = lines[i];
// Skip empty lines and comments
if (line.trim() === '' || line.trim().startsWith('#')) {
continue;
}
// Check indentation
const indent = line.match(/^(\s*)/)[1].length;
if (indent <= baseIndent) {
// Found next section at same or lower indentation
endIdx = i - 1;
break;
}
}
// If we didn't find an end, it goes to end of file
if (endIdx === startIdx) {
endIdx = lines.length - 1;
}
// Remove the web_bundle section (including the line before if it's just a blank line)
const newLines = [...lines.slice(0, startIdx), ...lines.slice(endIdx + 1)];
// Clean up any double blank lines that might result
const strippedYaml = newLines.join('\n').replaceAll(/\n\n\n+/g, '\n\n');
await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch {
// If anything fails, just copy the file as-is
console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`));
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
}
/**
* Process agent files to inject activation block
* @param {string} modulePath - Path to installed module