import { WFWorkflow, WFWorkflowAction, ValidationResult, ValidationError, ValidationWarning } from "../types.js";
import { getActionByIdentifier } from "../data/actions.js";
import { ACTION_PREFIX } from "../constants.js";
const UUID_PATTERN = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
function isValidUUID(uuid: string): boolean {
return UUID_PATTERN.test(uuid);
}
function validateStructure(workflow: WFWorkflow, errors: ValidationError[], warnings: ValidationWarning[]): void {
if (!workflow.WFWorkflowActions) {
errors.push({ type: "structure", message: "Missing WFWorkflowActions array" });
return;
}
if (!Array.isArray(workflow.WFWorkflowActions)) {
errors.push({ type: "structure", message: "WFWorkflowActions must be an array" });
return;
}
if (!workflow.WFWorkflowIcon) {
warnings.push({ type: "compatibility", message: "Missing WFWorkflowIcon" });
}
if (workflow.WFWorkflowMinimumClientVersion === undefined) {
warnings.push({ type: "compatibility", message: "Missing WFWorkflowMinimumClientVersion" });
}
}
function validateActions(actions: WFWorkflowAction[], errors: ValidationError[], warnings: ValidationWarning[]): void {
const uuids = new Set<string>();
const groups = new Map<string, { start: number; end: number }>();
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
if (!action.WFWorkflowActionIdentifier) {
errors.push({ type: "structure", message: `Action ${i} missing identifier`, actionIndex: i });
continue;
}
const id = action.WFWorkflowActionIdentifier;
if (!id.startsWith(ACTION_PREFIX) && !id.startsWith("com.")) {
warnings.push({ type: "compatibility", message: `Non-standard identifier at ${i}`, actionIndex: i });
}
if (!getActionByIdentifier(id) && id.startsWith(ACTION_PREFIX)) {
warnings.push({ type: "compatibility", message: `Unknown action at ${i}`, actionIndex: i });
}
if (!action.WFWorkflowActionParameters) action.WFWorkflowActionParameters = {};
const params = action.WFWorkflowActionParameters;
if (params.UUID) {
const uuid = params.UUID as string;
if (!isValidUUID(uuid)) {
errors.push({ type: "uuid", message: `Invalid UUID at ${i}`, actionIndex: i });
} else if (uuids.has(uuid)) {
errors.push({ type: "uuid", message: `Duplicate UUID at ${i}`, actionIndex: i });
} else {
uuids.add(uuid);
}
}
if (params.GroupingIdentifier) {
const gid = params.GroupingIdentifier as string;
const mode = params.WFControlFlowMode as number;
if (!groups.has(gid)) groups.set(gid, { start: 0, end: 0 });
const g = groups.get(gid)!;
if (mode === 0) g.start++;
else if (mode === 2) g.end++;
}
if (id === `${ACTION_PREFIX}downloadurl`) {
const url = params.WFURL as string;
if (url?.startsWith("http://")) {
warnings.push({ type: "security", message: `HTTP URL at ${i}`, actionIndex: i, suggestion: "Use HTTPS" });
}
}
}
for (const [gid, counts] of groups.entries()) {
if (counts.start !== counts.end) {
errors.push({ type: "logic", message: `Unbalanced control flow: ${gid}` });
}
}
}
function validateReferences(actions: WFWorkflowAction[], errors: ValidationError[], warnings: ValidationWarning[]): void {
const definedVars = new Set<string>();
const definedUUIDs = new Set<string>();
for (const action of actions) {
const params = action.WFWorkflowActionParameters;
if (action.WFWorkflowActionIdentifier === `${ACTION_PREFIX}setvariable`) {
const name = params.WFVariableName as string;
if (name) definedVars.add(name);
}
if (params.UUID) definedUUIDs.add(params.UUID as string);
}
const specialVars = ["Repeat Item", "Repeat Index", "Shortcut Input", "Current Date", "Clipboard"];
for (let i = 0; i < actions.length; i++) {
const params = actions[i].WFWorkflowActionParameters;
for (const [, value] of Object.entries(params)) {
if (typeof value === "object" && value !== null) {
const val = value as Record<string, unknown>;
if (val.WFSerializationType === "WFTextTokenAttachment") {
const inner = val.Value as Record<string, unknown>;
if (inner?.Type === "Variable") {
const name = inner.VariableName as string;
if (name && !definedVars.has(name) && !specialVars.includes(name)) {
warnings.push({ type: "compatibility", message: `Undefined variable '${name}' at ${i}`, actionIndex: i });
}
}
if (inner?.OutputUUID && !definedUUIDs.has(inner.OutputUUID as string)) {
errors.push({ type: "reference", message: `Undefined UUID reference at ${i}`, actionIndex: i });
}
}
}
}
}
}
export function validateShortcut(workflow: WFWorkflow): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
validateStructure(workflow, errors, warnings);
if (workflow.WFWorkflowActions && Array.isArray(workflow.WFWorkflowActions)) {
validateActions(workflow.WFWorkflowActions, errors, warnings);
validateReferences(workflow.WFWorkflowActions, errors, warnings);
}
return { valid: errors.length === 0, errors, warnings };
}
export function formatValidationResult(result: ValidationResult): string {
const lines: string[] = [];
if (result.valid) {
lines.push("# Valid", "", "Shortcut structure is valid.");
} else {
lines.push("# Invalid", "", `${result.errors.length} error(s) found.`);
}
if (result.errors.length > 0) {
lines.push("", "## Errors");
for (const e of result.errors) {
lines.push(`- ${e.type}: ${e.message}`);
}
}
if (result.warnings.length > 0) {
lines.push("", `## Warnings (${result.warnings.length})`);
for (const w of result.warnings) {
lines.push(`- ${w.type}: ${w.message}${w.suggestion ? ` (${w.suggestion})` : ""}`);
}
}
return lines.join("\n");
}