import { WFWorkflow, WFWorkflowAction, ShortcutAnalysis, VariableUsage } from "../types.js";
import { getActionByIdentifier } from "../data/actions.js";
import { ACTION_PREFIX } from "../constants.js";
function getActionCategory(identifier: string): string {
return getActionByIdentifier(identifier)?.category ?? "Unknown";
}
function extractVariables(action: WFWorkflowAction): { sets: string[]; gets: string[] } {
const sets: string[] = [];
const gets: string[] = [];
const params = action.WFWorkflowActionParameters;
if (action.WFWorkflowActionIdentifier === `${ACTION_PREFIX}setvariable`) {
const name = params.WFVariableName as string;
if (name) sets.push(name);
}
if (action.WFWorkflowActionIdentifier === `${ACTION_PREFIX}getvariable`) {
const variable = params.WFVariable as Record<string, unknown>;
const value = variable?.Value as Record<string, unknown>;
if (value?.VariableName) gets.push(value.VariableName as string);
}
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" && inner.VariableName) {
gets.push(inner.VariableName as string);
}
}
}
}
return { sets, gets };
}
function identifyPatterns(actions: WFWorkflowAction[]): string[] {
const patterns: string[] = [];
const ids = actions.map(a => a.WFWorkflowActionIdentifier);
if (ids.includes(`${ACTION_PREFIX}downloadurl`) && (ids.includes(`${ACTION_PREFIX}dictionary`) || ids.includes(`${ACTION_PREFIX}detect.dictionary`))) {
patterns.push("API Integration");
}
if (ids.includes(`${ACTION_PREFIX}conditional`) && ids.includes(`${ACTION_PREFIX}notification`)) {
patterns.push("Error Handling");
}
if (ids.includes(`${ACTION_PREFIX}choosefrommenu`)) patterns.push("Menu-Driven");
if ((ids.includes(`${ACTION_PREFIX}text.split`) || ids.includes(`${ACTION_PREFIX}repeat.each`)) && ids.includes(`${ACTION_PREFIX}text.combine`)) {
patterns.push("Data Processing");
}
if (ids.includes(`${ACTION_PREFIX}runshortcut`)) patterns.push("Modular Design");
if (ids.includes(`${ACTION_PREFIX}text.match`) || ids.includes(`${ACTION_PREFIX}text.replace`)) patterns.push("Text Processing");
const photoActions = [`${ACTION_PREFIX}takephoto`, `${ACTION_PREFIX}selectphoto`, `${ACTION_PREFIX}image.resize`, `${ACTION_PREFIX}image.convert`];
if (ids.some(id => photoActions.includes(id))) patterns.push("Photo Manipulation");
return patterns;
}
function generateSuggestions(actions: WFWorkflowAction[], variables: VariableUsage[], categories: Record<string, number>): string[] {
const suggestions: string[] = [];
const ids = actions.map(a => a.WFWorkflowActionIdentifier);
const urlActions = actions.filter(a => a.WFWorkflowActionIdentifier === `${ACTION_PREFIX}downloadurl`);
if (urlActions.some(a => a.WFWorkflowActionParameters.WFHTTPHeaders)) {
suggestions.push("Consider using variables for API keys instead of hardcoding");
}
if (ids.includes(`${ACTION_PREFIX}downloadurl`) && !ids.includes(`${ACTION_PREFIX}conditional`)) {
suggestions.push("Add error handling for API calls");
}
for (const v of variables) {
if (v.setAt.length > 0 && v.usedAt.length === 0) {
suggestions.push(`Variable '${v.name}' is set but never used`);
}
}
if (actions.length > 50) suggestions.push("Consider breaking into smaller shortcuts");
if (categories["Web & APIs"] && !ids.includes(`${ACTION_PREFIX}notification`)) {
suggestions.push("Add notifications for long-running operations");
}
return suggestions;
}
function countNestedLoops(actions: WFWorkflowAction[]): number {
let max = 0, current = 0;
for (const action of actions) {
const id = action.WFWorkflowActionIdentifier;
const mode = action.WFWorkflowActionParameters.WFControlFlowMode;
if ((id === `${ACTION_PREFIX}repeat.each` || id === `${ACTION_PREFIX}repeat.count`)) {
if (mode === 0) { current++; max = Math.max(max, current); }
else if (mode === 2) current--;
}
}
return max;
}
function detectPermissions(actions: WFWorkflowAction[]): string[] {
const perms: string[] = [];
const ids = new Set(actions.map(a => a.WFWorkflowActionIdentifier));
const map: Record<string, string> = {
[`${ACTION_PREFIX}getcurrentlocation`]: "Location",
[`${ACTION_PREFIX}takephoto`]: "Camera",
[`${ACTION_PREFIX}selectphoto`]: "Photos",
[`${ACTION_PREFIX}getlatestphotos`]: "Photos",
[`${ACTION_PREFIX}contacts.get`]: "Contacts",
[`${ACTION_PREFIX}calendar.add`]: "Calendar",
[`${ACTION_PREFIX}reminders.add`]: "Reminders",
[`${ACTION_PREFIX}health.log`]: "Health",
[`${ACTION_PREFIX}homekit.control`]: "HomeKit",
[`${ACTION_PREFIX}sendmessage`]: "Messages",
[`${ACTION_PREFIX}sendemail`]: "Mail",
[`${ACTION_PREFIX}call`]: "Phone",
[`${ACTION_PREFIX}notification`]: "Notifications",
};
for (const [actionId, perm] of Object.entries(map)) {
if (ids.has(actionId) && !perms.includes(perm)) perms.push(perm);
}
return perms;
}
function detectCompatibility(actions: WFWorkflowAction[]): { ios: boolean; macos: boolean; minVersion: string } {
let ios = true, macos = true, minVersion = "1.0";
const iosOnly = [`${ACTION_PREFIX}takephoto`, `${ACTION_PREFIX}call`, `${ACTION_PREFIX}facetime`];
const macOnly = [`${ACTION_PREFIX}runshellscript`];
for (const action of actions) {
const id = action.WFWorkflowActionIdentifier;
if (iosOnly.includes(id)) macos = false;
if (macOnly.includes(id)) ios = false;
}
const ids = new Set(actions.map(a => a.WFWorkflowActionIdentifier));
if (ids.has(`${ACTION_PREFIX}focus.set`)) minVersion = "15.0";
return { ios, macos, minVersion };
}
function calculateComplexity(actionCount: number, varCount: number, patternCount: number, loopDepth: number): "simple" | "moderate" | "complex" {
let score = 0;
if (actionCount > 30) score += 2; else if (actionCount > 15) score += 1;
if (varCount > 10) score += 2; else if (varCount > 5) score += 1;
score += patternCount * 0.5 + loopDepth * 1.5;
if (score >= 5) return "complex";
if (score >= 2) return "moderate";
return "simple";
}
function estimateRuntime(actions: WFWorkflowAction[]): string {
let seconds = 0;
for (const action of actions) {
const id = action.WFWorkflowActionIdentifier;
if (id === `${ACTION_PREFIX}downloadurl`) seconds += 2;
else if (id === `${ACTION_PREFIX}wait`) seconds += (action.WFWorkflowActionParameters.WFDelayTime as number) || 0;
else if (id.includes("photo") || id.includes("image")) seconds += 0.5;
else if (id === `${ACTION_PREFIX}runshortcut`) seconds += 1;
else seconds += 0.1;
}
if (seconds < 1) return "< 1s";
if (seconds < 5) return "1-5s";
if (seconds < 15) return "5-15s";
if (seconds < 60) return "15-60s";
return "> 1 min";
}
export function analyzeShortcut(workflow: WFWorkflow, name = "Untitled"): ShortcutAnalysis {
const actions = workflow.WFWorkflowActions;
const categories: Record<string, number> = {};
for (const action of actions) {
const cat = getActionCategory(action.WFWorkflowActionIdentifier);
categories[cat] = (categories[cat] || 0) + 1;
}
const variableMap = new Map<string, VariableUsage>();
actions.forEach((action, i) => {
const { sets, gets } = extractVariables(action);
for (const n of sets) {
if (!variableMap.has(n)) variableMap.set(n, { name: n, type: "unknown", setAt: [], usedAt: [] });
variableMap.get(n)!.setAt.push(i);
}
for (const n of gets) {
if (!variableMap.has(n)) variableMap.set(n, { name: n, type: "unknown", setAt: [], usedAt: [] });
variableMap.get(n)!.usedAt.push(i);
}
});
const variables = Array.from(variableMap.values());
const patterns = identifyPatterns(actions);
const loopDepth = countNestedLoops(actions);
return {
name,
actionCount: actions.length,
categories,
variables,
complexity: calculateComplexity(actions.length, variables.length, patterns.length, loopDepth),
estimatedRuntime: estimateRuntime(actions),
permissions: detectPermissions(actions),
compatibility: detectCompatibility(actions),
patterns,
suggestions: generateSuggestions(actions, variables, categories)
};
}