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 { parse as parseYaml } from "yaml"; export function resolveHomePath(input: string): string { if (input === "~") { return os.homedir(); } if (input.startsWith("~/")) { return path.join(os.homedir(), input.slice(2)); } return path.resolve(input); } export async function exists(filePath: string): Promise { return await pathExists(filePath); } export async function isDirectory(dirPath: string): Promise { try { return (await fs.stat(dirPath)).isDirectory(); } catch { return false; } } export function sanitizeName(name: string): string { return name .trim() .toLowerCase() .replaceAll(/[^a-z0-9._-]+/g, "-") .replaceAll(/^-+|-+$/g, ""); } export async function readText(filePath: string | undefined): Promise { if (!filePath) { return undefined; } try { return await fs.readFile(filePath, "utf8"); } catch { return 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]; let value = match[2] ?? ""; 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 function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } export function childRecord( root: Record | undefined, key: string, ): Record { const value = root?.[key]; return isRecord(value) ? value : {}; } export function readString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value.trim() : undefined; } 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)); } }