// Migrate Hermes helper module supports helpers behavior. import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { markMigrationItemError, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET, } from "openclaw/plugin-sdk/migration"; import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry"; import { appendRegularFile, pathExists } from "openclaw/plugin-sdk/security-runtime"; import { isRecord as sharedIsRecord, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { parse as parseYaml } from "yaml"; const HOME_SHORTHAND_RE = /^~(?=$|[\\/])/u; const UNSAFE_NAME_CHARS_RE = /[^a-z0-9._-]+/g; const EDGE_DASHES_RE = /^-+|-+$/g; export function resolveHomePath(input: string): string { const value = input.trim(); return value ? path.resolve(value.replace(HOME_SHORTHAND_RE, os.homedir())) : value; } export async function exists(filePath: string): Promise { return await pathExists(filePath); } export async function isDirectory(dirPath: string): Promise { const stat = await fs.stat(dirPath).catch(() => undefined); return stat?.isDirectory() === true; } export function sanitizeName(name: string): string { const normalized = name.trim().toLowerCase().replaceAll(UNSAFE_NAME_CHARS_RE, "-"); return normalized.replaceAll(EDGE_DASHES_RE, ""); } export async function readText(filePath: string | undefined): Promise { return filePath ? await fs.readFile(filePath, "utf8").catch(() => undefined) : undefined; } export function parseEnv(content: string | undefined): Record { const env: Record = {}; if (!content) { return env; } for (const line of content.split(/\r?\n/u)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { continue; } const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed); if (!match) { continue; } const key = match[1]; const rawValue = match[2]; if (!key || rawValue === undefined) { continue; } let value = rawValue; if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } env[key] = value; } return env; } export function parseHermesConfig(content: string | undefined): Record { if (!content) { return {}; } try { const parsed = parseYaml(content); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : {}; } catch { return {}; } } export const isRecord = sharedIsRecord; export function childRecord( root: Record | undefined, key: string, ): Record { const value = root?.[key]; return isRecord(value) ? value : {}; } export const readString = normalizeOptionalString; export function readStringArray(value: unknown): string[] { if (!Array.isArray(value)) { return []; } return value.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== ""); } export async function appendItem(item: MigrationItem): Promise { if (!item.source || !item.target) { return markMigrationItemError(item, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET); } try { const content = await fs.readFile(item.source, "utf8"); const header = `\n\n\n\n`; await fs.mkdir(path.dirname(item.target), { recursive: true }); await appendRegularFile({ filePath: item.target, content: `${header}${content.trimEnd()}\n`, rejectSymlinkParents: true, }); return { ...item, status: "migrated" }; } catch (err) { return markMigrationItemError(item, err instanceof Error ? err.message : String(err)); } }