@workflow-manager/runner
Version:
CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes
100 lines (99 loc) • 3.4 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
function appendRecord(filePath, record) {
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, { encoding: "utf-8", mode: 0o600 });
}
export function defaultRunArchiveDirectory() {
return path.join(process.cwd(), ".wfm", "runs");
}
export function createRunArchive(workflow, runId, directory = defaultRunArchiveDirectory()) {
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
fs.chmodSync(directory, 0o700);
const archivePath = path.join(directory, `${runId}.jsonl`);
fs.writeFileSync(archivePath, "", { encoding: "utf-8", mode: 0o600 });
fs.chmodSync(archivePath, 0o600);
const archive = new RunArchive(archivePath);
archive.writeMetadata(workflow, runId);
return Object.assign(archive, { path: archivePath, runId });
}
export class RunArchive {
path;
constructor(path) {
this.path = path;
}
writeMetadata(workflow, runId) {
appendRecord(this.path, {
type: "metadata",
occurredAt: new Date().toISOString(),
data: {
runId,
workflowKey: workflow.key,
workflowTitle: workflow.title,
},
});
}
onEvent(event) {
appendRecord(this.path, {
type: "event",
occurredAt: event.occurredAt,
data: {
id: event.id,
sequence: event.sequenceNumber,
type: event.type,
runId: event.runId,
stepKey: event.stepRunId,
actor: event.actor,
payload: event.payload,
},
});
}
onSnapshot(snapshot, stepDetails) {
appendRecord(this.path, {
type: "snapshot",
occurredAt: snapshot.updatedAt ?? new Date().toISOString(),
data: {
snapshot,
stepDetails,
},
});
}
onLog() { }
}
export function readRunArchive(filePath) {
const resolvedPath = path.resolve(filePath);
let raw;
try {
raw = fs.readFileSync(resolvedPath, "utf-8");
}
catch (error) {
return `Could not read run archive ${resolvedPath}: ${error.message}`;
}
const records = [];
for (const [index, line] of raw.split("\n").entries()) {
if (!line.trim())
continue;
try {
const parsed = JSON.parse(line);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return `Run archive ${resolvedPath} has an invalid record on line ${index + 1}`;
}
const record = parsed;
if ((record.type !== "metadata" && record.type !== "event" && record.type !== "snapshot") ||
typeof record.occurredAt !== "string" ||
!record.data ||
typeof record.data !== "object" ||
Array.isArray(record.data)) {
return `Run archive ${resolvedPath} has an invalid record on line ${index + 1}`;
}
records.push({
type: record.type,
occurredAt: record.occurredAt,
data: record.data,
});
}
catch {
return `Run archive ${resolvedPath} is not valid JSONL (line ${index + 1})`;
}
}
return records;
}