scai
Version:
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.
104 lines (103 loc) • 4.08 kB
JavaScript
// File: src/workflow/workflowRunner.ts
import fs from "fs/promises";
import chalk from "chalk";
import { normalizePath } from "../utils/contentUtils.js";
/**
* runWorkflow - orchestrates running a modular I/O pipeline.
*
* Each module conforms to the `ModuleIO` interface:
* input: { query, content? }
* output: { data?, mode?, newFilepath? }
*
* Options:
* - modules: ordered modules to run (Module[])
* - filepath: optional file path to read/write
* - inputContent: optional string for stdin content
*
* Behaviours:
* - Sequentially runs each module on the full file content
* - Supports output modes: overwrite, append, newFile, skip
* - Stream mode (no filepath): prints final output to stdout
*/
export async function runWorkflow(opts) {
const { modules } = opts;
let filepath = opts.filepath;
let fileContent = "";
try {
if (filepath) {
filepath = normalizePath(filepath);
await fs.access(filepath);
fileContent = await fs.readFile(filepath, "utf-8");
}
else {
fileContent = opts.inputContent ?? "";
}
for (const mod of modules) {
console.log(chalk.cyan(`\n⚙️ Running module: ${mod.name}`));
const io = {
query: `Process ${filepath ?? "<stdin>"} with ${mod.name}`,
content: fileContent,
};
const result = await mod.run(io);
if (!result || !result.data || !String(result.data).trim()) {
throw new Error(`⚠️ Empty result from module ${mod.name}`);
}
const output = typeof result.data === "string"
? result.data
: JSON.stringify(result.data, null, 2);
const mode = result.mode ??
"overwrite"; // default mode if not specified by module
const newFilepath = result.newFilepath;
//
// === Handle stdout vs file modes ===
//
if (!filepath) {
// stdin mode: print directly to stdout
process.stdout.write(output);
fileContent = output;
continue;
}
//
// === File-backed mode handling (honor mode semantics) ===
//
switch (mode) {
case "overwrite":
await fs.writeFile(filepath, output, "utf-8");
console.log(chalk.green(`✅ Overwritten: ${filepath}`));
fileContent = output;
break;
case "append":
await fs.appendFile(filepath, output, "utf-8");
console.log(chalk.green(`✅ Appended: ${filepath}`));
fileContent += output;
break;
case "newFile":
if (!newFilepath)
throw new Error(`newFile mode requires a newFilepath`);
const resolvedNew = normalizePath(newFilepath);
await fs.writeFile(resolvedNew, output, "utf-8");
console.log(chalk.green(`✅ New file created: ${resolvedNew}`));
filepath = resolvedNew;
fileContent = output;
break;
case "skip":
console.log(chalk.gray(`⏭️ Skipped writing for module ${mod.name}`));
fileContent = output;
break;
default:
console.log(chalk.yellow(`⚠️ Unknown mode (${String(mode)}). Treating as overwrite.`));
await fs.writeFile(filepath, output, "utf-8");
fileContent = output;
break;
}
}
// Final stdout flush if running without filepath
if (!opts.filepath) {
process.stdout.write("\n");
}
}
catch (err) {
console.error(chalk.red("❌ Error in workflow run:"), err?.message ?? err);
throw err;
}
}