Files
openclaw/src/plugins/installed-plugin-index-store.ts
T
2026-04-26 02:31:02 +01:00

213 lines
6.9 KiB
TypeScript

import { z } from "zod";
import { saveJsonFile } from "../infra/json-file.js";
import { readJsonFile, readJsonFileSync, writeJsonAtomic } from "../infra/json-files.js";
import { safeParseWithSchema } from "../utils/zod-parse.js";
import {
resolveInstalledPluginIndexStorePath,
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store-path.js";
import {
diffInstalledPluginIndexInvalidationReasons,
extractPluginInstallRecordsFromInstalledPluginIndex,
INSTALLED_PLUGIN_INDEX_WARNING,
INSTALLED_PLUGIN_INDEX_VERSION,
INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION,
loadInstalledPluginIndex,
refreshInstalledPluginIndex,
type InstalledPluginIndex,
type InstalledPluginIndexRefreshReason,
type LoadInstalledPluginIndexParams,
type RefreshInstalledPluginIndexParams,
} from "./installed-plugin-index.js";
export {
INSTALLED_PLUGIN_INDEX_STORE_PATH,
resolveInstalledPluginIndexStorePath,
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store-path.js";
export type InstalledPluginIndexStoreState = "missing" | "fresh" | "stale";
export type InstalledPluginIndexStoreInspection = {
state: InstalledPluginIndexStoreState;
refreshReasons: readonly InstalledPluginIndexRefreshReason[];
persisted: InstalledPluginIndex | null;
current: InstalledPluginIndex;
};
const StringArraySchema = z.array(z.string());
const InstalledPluginIndexStartupSchema = z
.object({
sidecar: z.boolean(),
memory: z.boolean(),
deferConfiguredChannelFullLoadUntilAfterListen: z.boolean(),
agentHarnesses: StringArraySchema,
})
.passthrough();
const InstalledPluginIndexRecordSchema = z
.object({
pluginId: z.string(),
packageName: z.string().optional(),
packageVersion: z.string().optional(),
installRecord: z.record(z.string(), z.unknown()).optional(),
installRecordHash: z.string().optional(),
packageInstall: z.unknown().optional(),
manifestPath: z.string(),
manifestHash: z.string(),
format: z.string().optional(),
bundleFormat: z.string().optional(),
source: z.string().optional(),
setupSource: z.string().optional(),
packageJson: z
.object({
path: z.string(),
hash: z.string(),
})
.optional(),
rootDir: z.string(),
origin: z.string(),
enabled: z.boolean(),
enabledByDefault: z.boolean().optional(),
startup: InstalledPluginIndexStartupSchema,
compat: z.array(z.string()),
})
.passthrough();
const InstalledPluginInstallRecordSchema = z.record(z.string(), z.unknown());
const PluginDiagnosticSchema = z
.object({
level: z.union([z.literal("warn"), z.literal("error")]),
message: z.string(),
pluginId: z.string().optional(),
source: z.string().optional(),
})
.passthrough();
const InstalledPluginIndexSchema = z
.object({
version: z.literal(INSTALLED_PLUGIN_INDEX_VERSION),
warning: z.string().optional(),
hostContractVersion: z.string(),
compatRegistryVersion: z.string(),
migrationVersion: z.literal(INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION),
policyHash: z.string(),
generatedAtMs: z.number(),
refreshReason: z.string().optional(),
installRecords: z.record(z.string(), InstalledPluginInstallRecordSchema).optional(),
plugins: z.array(InstalledPluginIndexRecordSchema),
diagnostics: z.array(PluginDiagnosticSchema),
})
.passthrough();
function parseInstalledPluginIndex(value: unknown): InstalledPluginIndex | null {
const parsed = safeParseWithSchema(InstalledPluginIndexSchema, value) as
| (Omit<InstalledPluginIndex, "installRecords"> & {
installRecords?: InstalledPluginIndex["installRecords"];
})
| null;
if (!parsed) {
return null;
}
return {
...parsed,
installRecords:
parsed.installRecords ??
extractPluginInstallRecordsFromInstalledPluginIndex(parsed as InstalledPluginIndex),
};
}
export async function readPersistedInstalledPluginIndex(
options: InstalledPluginIndexStoreOptions = {},
): Promise<InstalledPluginIndex | null> {
const parsed = await readJsonFile<unknown>(resolveInstalledPluginIndexStorePath(options));
return parseInstalledPluginIndex(parsed);
}
export function readPersistedInstalledPluginIndexSync(
options: InstalledPluginIndexStoreOptions = {},
): InstalledPluginIndex | null {
const parsed = readJsonFileSync(resolveInstalledPluginIndexStorePath(options));
return parseInstalledPluginIndex(parsed);
}
export async function writePersistedInstalledPluginIndex(
index: InstalledPluginIndex,
options: InstalledPluginIndexStoreOptions = {},
): Promise<string> {
const filePath = resolveInstalledPluginIndexStorePath(options);
await writeJsonAtomic(
filePath,
{ ...index, warning: INSTALLED_PLUGIN_INDEX_WARNING },
{
trailingNewline: true,
ensureDirMode: 0o700,
mode: 0o600,
},
);
return filePath;
}
export function writePersistedInstalledPluginIndexSync(
index: InstalledPluginIndex,
options: InstalledPluginIndexStoreOptions = {},
): string {
const filePath = resolveInstalledPluginIndexStorePath(options);
saveJsonFile(filePath, { ...index, warning: INSTALLED_PLUGIN_INDEX_WARNING });
return filePath;
}
export async function inspectPersistedInstalledPluginIndex(
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
): Promise<InstalledPluginIndexStoreInspection> {
const persisted = await readPersistedInstalledPluginIndex(params);
const current = loadInstalledPluginIndex({
...params,
installRecords:
params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted),
});
if (!persisted) {
return {
state: "missing",
refreshReasons: ["missing"],
persisted: null,
current,
};
}
const refreshReasons = diffInstalledPluginIndexInvalidationReasons(persisted, current);
return {
state: refreshReasons.length > 0 ? "stale" : "fresh",
refreshReasons,
persisted,
current,
};
}
export async function refreshPersistedInstalledPluginIndex(
params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions,
): Promise<InstalledPluginIndex> {
const persisted = params.installRecords ? null : await readPersistedInstalledPluginIndex(params);
const index = refreshInstalledPluginIndex({
...params,
installRecords:
params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted),
});
await writePersistedInstalledPluginIndex(index, params);
return index;
}
export function refreshPersistedInstalledPluginIndexSync(
params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions,
): InstalledPluginIndex {
const persisted = params.installRecords ? null : readPersistedInstalledPluginIndexSync(params);
const index = refreshInstalledPluginIndex({
...params,
installRecords:
params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted),
});
writePersistedInstalledPluginIndexSync(index, params);
return index;
}