Download src/utils/automation-export.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 7.99 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/utils/automation-export.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/utils/automation-export.ts
-
curl -L -o automation-export.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/utils/automation-export.ts
7.99 kB
| import { getImportExportSpec } from "#/manifests/automation-interface"; | |
| import type { | |
| Automation, | |
| AutomationExportFile, | |
| AutomationSpec, | |
| AutomationTrigger, | |
| } from "#/types/automation"; | |
| const SUPPORTED_TRIGGER_TYPES = new Set(["cron", "schedule", "event"]); | |
| type UnknownRecord = Record<string, unknown>; | |
| export class AutomationFileValidationError extends Error { | |
| readonly issues: string[]; | |
| constructor(issues: string[]) { | |
| super(issues.join("\n")); | |
| this.name = "AutomationFileValidationError"; | |
| this.issues = issues; | |
| } | |
| } | |
| function isRecord(value: unknown): value is UnknownRecord { | |
| return typeof value === "object" && value !== null && !Array.isArray(value); | |
| } | |
| function validateRequiredString( | |
| record: UnknownRecord, | |
| field: string, | |
| path: string, | |
| issues: string[], | |
| ): string | undefined { | |
| const value = record[field]; | |
| if (typeof value !== "string" || value.trim().length === 0) { | |
| issues.push(`${path}: expected a non-empty string`); | |
| return undefined; | |
| } | |
| return value; | |
| } | |
| function validateOptionalString( | |
| record: UnknownRecord, | |
| field: string, | |
| path: string, | |
| issues: string[], | |
| ): string | undefined { | |
| const value = record[field]; | |
| if (value === undefined) return undefined; | |
| if (typeof value !== "string") { | |
| issues.push(`${path}: expected a string`); | |
| return undefined; | |
| } | |
| return value; | |
| } | |
| function validateTrigger( | |
| value: unknown, | |
| issues: string[], | |
| ): AutomationTrigger | undefined { | |
| if (!isRecord(value)) { | |
| issues.push("spec.trigger: expected an object"); | |
| return undefined; | |
| } | |
| const type = validateRequiredString( | |
| value, | |
| "type", | |
| "spec.trigger.type", | |
| issues, | |
| ); | |
| if (type && !SUPPORTED_TRIGGER_TYPES.has(type)) { | |
| issues.push( | |
| 'spec.trigger.type: expected one of "cron", "schedule", or "event"', | |
| ); | |
| } | |
| const schedule = validateOptionalString( | |
| value, | |
| "schedule", | |
| "spec.trigger.schedule", | |
| issues, | |
| ); | |
| const scheduleHuman = validateOptionalString( | |
| value, | |
| "schedule_human", | |
| "spec.trigger.schedule_human", | |
| issues, | |
| ); | |
| const timezone = validateOptionalString( | |
| value, | |
| "timezone", | |
| "spec.trigger.timezone", | |
| issues, | |
| ); | |
| const source = validateOptionalString( | |
| value, | |
| "source", | |
| "spec.trigger.source", | |
| issues, | |
| ); | |
| const filter = validateOptionalString( | |
| value, | |
| "filter", | |
| "spec.trigger.filter", | |
| issues, | |
| ); | |
| let on: string | string[] | undefined; | |
| if (value.on !== undefined) { | |
| if ( | |
| typeof value.on === "string" || | |
| (Array.isArray(value.on) && | |
| value.on.length > 0 && | |
| value.on.every( | |
| (entry) => typeof entry === "string" && entry.trim().length > 0, | |
| )) | |
| ) { | |
| on = value.on as string | string[]; | |
| } else { | |
| issues.push( | |
| "spec.trigger.on: expected a string or a non-empty array of strings", | |
| ); | |
| } | |
| } | |
| if ((type === "cron" || type === "schedule") && !schedule?.trim()) { | |
| issues.push("spec.trigger.schedule: required for a scheduled trigger"); | |
| } | |
| if (type === "event") { | |
| if (!source?.trim()) { | |
| issues.push("spec.trigger.source: required for an event trigger"); | |
| } | |
| if ( | |
| on === undefined || | |
| (typeof on === "string" && on.trim().length === 0) | |
| ) { | |
| issues.push("spec.trigger.on: required for an event trigger"); | |
| } | |
| } | |
| if (!type || !SUPPORTED_TRIGGER_TYPES.has(type)) return undefined; | |
| return { | |
| type, | |
| ...(schedule !== undefined && { schedule }), | |
| ...(scheduleHuman !== undefined && { schedule_human: scheduleHuman }), | |
| ...(timezone !== undefined && { timezone }), | |
| ...(source !== undefined && { source }), | |
| ...(on !== undefined && { on }), | |
| ...(filter !== undefined && { filter }), | |
| }; | |
| } | |
| export function serializeAutomation(a: Automation): AutomationExportFile { | |
| const timezone = a.timezone ?? a.trigger.timezone; | |
| const spec: AutomationSpec = { | |
| name: a.name, | |
| trigger: { ...a.trigger }, | |
| enabled: a.enabled, | |
| prompt: a.prompt, | |
| ...(a.repository !== undefined && { repository: a.repository }), | |
| ...(a.model !== undefined && { model: a.model }), | |
| ...(a.timeout != null && { timeout: a.timeout }), | |
| ...(a.branch !== undefined && { branch: a.branch }), | |
| ...(a.plugins !== undefined && { plugins: [...a.plugins] }), | |
| ...(a.notification !== undefined && { notification: a.notification }), | |
| ...(timezone !== undefined && { timezone }), | |
| }; | |
| const envelope = getImportExportSpec(); | |
| return { version: envelope.fileVersion, kind: envelope.fileKind, spec }; | |
| } | |
| export function getAutomationExportFilename( | |
| automation: Pick<Automation, "id" | "name">, | |
| ): string { | |
| const slug = automation.name | |
| .trim() | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, ""); | |
| return `${slug || automation.id}${getImportExportSpec().filenameSuffix}`; | |
| } | |
| export function parseAutomationFile(json: unknown): AutomationSpec { | |
| const issues: string[] = []; | |
| if (!isRecord(json)) { | |
| throw new AutomationFileValidationError(["file: expected a JSON object"]); | |
| } | |
| const envelope = getImportExportSpec(); | |
| if (json.version !== envelope.fileVersion) { | |
| issues.push(`version: expected ${envelope.fileVersion}`); | |
| } | |
| if (json.kind !== envelope.fileKind) { | |
| issues.push(`kind: expected "${envelope.fileKind}"`); | |
| } | |
| if (!isRecord(json.spec)) { | |
| issues.push("spec: expected an object"); | |
| throw new AutomationFileValidationError(issues); | |
| } | |
| const name = validateRequiredString(json.spec, "name", "spec.name", issues); | |
| const prompt = validateRequiredString( | |
| json.spec, | |
| "prompt", | |
| "spec.prompt", | |
| issues, | |
| ); | |
| const trigger = validateTrigger(json.spec.trigger, issues); | |
| const enabled = json.spec.enabled; | |
| if (typeof enabled !== "boolean") { | |
| issues.push("spec.enabled: expected a boolean"); | |
| } | |
| const repository = validateOptionalString( | |
| json.spec, | |
| "repository", | |
| "spec.repository", | |
| issues, | |
| ); | |
| const branch = validateOptionalString( | |
| json.spec, | |
| "branch", | |
| "spec.branch", | |
| issues, | |
| ); | |
| const notification = validateOptionalString( | |
| json.spec, | |
| "notification", | |
| "spec.notification", | |
| issues, | |
| ); | |
| const timezone = validateOptionalString( | |
| json.spec, | |
| "timezone", | |
| "spec.timezone", | |
| issues, | |
| ); | |
| let model: string | null | undefined; | |
| if (json.spec.model === null || json.spec.model === undefined) { | |
| model = json.spec.model; | |
| } else if ( | |
| typeof json.spec.model === "string" && | |
| json.spec.model.trim().length > 0 | |
| ) { | |
| model = json.spec.model; | |
| } else { | |
| issues.push("spec.model: expected a non-empty string or null"); | |
| } | |
| let timeout: number | null | undefined; | |
| if (json.spec.timeout === null || json.spec.timeout === undefined) { | |
| timeout = json.spec.timeout; | |
| } else if ( | |
| typeof json.spec.timeout === "number" && | |
| Number.isInteger(json.spec.timeout) && | |
| json.spec.timeout > 0 | |
| ) { | |
| timeout = json.spec.timeout; | |
| } else { | |
| issues.push("spec.timeout: expected a positive integer"); | |
| } | |
| let plugins: string[] | undefined; | |
| if (json.spec.plugins !== undefined) { | |
| if ( | |
| Array.isArray(json.spec.plugins) && | |
| json.spec.plugins.every( | |
| (plugin) => typeof plugin === "string" && plugin.trim().length > 0, | |
| ) | |
| ) { | |
| plugins = [...json.spec.plugins] as string[]; | |
| } else { | |
| issues.push("spec.plugins: expected an array of non-empty strings"); | |
| } | |
| } | |
| if (issues.length > 0 || !name || !prompt || !trigger) { | |
| throw new AutomationFileValidationError(issues); | |
| } | |
| return { | |
| name, | |
| prompt, | |
| trigger, | |
| enabled: enabled as boolean, | |
| ...(repository !== undefined && { repository }), | |
| ...(model !== undefined && { model }), | |
| ...(timeout !== undefined && { timeout }), | |
| ...(branch !== undefined && { branch }), | |
| ...(plugins !== undefined && { plugins }), | |
| ...(notification !== undefined && { notification }), | |
| ...(timezone !== undefined && { timezone }), | |
| }; | |
| } | |