feat(omo-codex): batch 94 (13 files)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { isRecord } from "./utils.mjs";
|
||||
|
||||
const DEFAULT_MARKETPLACE_PATH = "packages/omo-codex/marketplace.json";
|
||||
|
||||
export async function readMarketplace(repoRoot, options = {}) {
|
||||
const marketplacePath = options.marketplacePath ?? join(repoRoot, DEFAULT_MARKETPLACE_PATH);
|
||||
const raw = await readFile(marketplacePath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!isRecord(parsed)) throw new Error("marketplace.json must be an object");
|
||||
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
|
||||
throw new Error("marketplace.json name must be a non-empty string");
|
||||
}
|
||||
validatePathSegment(parsed.name, "marketplace name");
|
||||
if (!Array.isArray(parsed.plugins)) throw new Error("marketplace.json plugins must be an array");
|
||||
|
||||
return {
|
||||
name: parsed.name,
|
||||
plugins: parsed.plugins.map((plugin, index) => normalizeMarketplacePlugin(plugin, index)),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginSource(marketplaceRoot, plugin, options = {}) {
|
||||
const sourcePath = localSourcePath(options.pathOverride ?? plugin.source);
|
||||
const relativePath = sourcePath.slice(2);
|
||||
return join(marketplaceRoot, ...relativePath.split(/[\\/]/));
|
||||
}
|
||||
|
||||
export async function readPluginManifest(pluginRoot) {
|
||||
const raw = await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!isRecord(parsed)) throw new Error(`${pluginRoot} plugin.json must be an object`);
|
||||
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
|
||||
throw new Error(`${pluginRoot} plugin.json name must be a non-empty string`);
|
||||
}
|
||||
const manifest = { name: parsed.name };
|
||||
if (parsed.version !== undefined) {
|
||||
if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
|
||||
throw new Error(`${pluginRoot} plugin.json version must be a non-empty string`);
|
||||
}
|
||||
manifest.version = parsed.version.trim();
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export function validatePathSegment(value, label) {
|
||||
if (!/^[A-Za-z0-9._+-]+$/.test(value)) {
|
||||
throw new Error(`${label} contains unsupported characters: ${value}`);
|
||||
}
|
||||
if (value === "." || value === "..") {
|
||||
throw new Error(`${label} must not be a path traversal segment`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMarketplacePlugin(plugin, index) {
|
||||
if (!isRecord(plugin)) throw new Error(`marketplace plugin ${index} must be an object`);
|
||||
if (typeof plugin.name !== "string" || plugin.name.trim() === "") {
|
||||
throw new Error(`marketplace plugin ${index} name must be a non-empty string`);
|
||||
}
|
||||
validatePathSegment(plugin.name, "plugin name");
|
||||
if (plugin.source === undefined || typeof plugin.source === "string") {
|
||||
if (typeof plugin.source === "string") validateLocalSourcePath(plugin.source);
|
||||
return {
|
||||
name: plugin.name,
|
||||
source: plugin.source,
|
||||
};
|
||||
}
|
||||
if (isRecord(plugin.source) && plugin.source.source === "local" && typeof plugin.source.path === "string") {
|
||||
validateLocalSourcePath(plugin.source.path);
|
||||
return {
|
||||
name: plugin.name,
|
||||
source: { source: "local", path: plugin.source.path },
|
||||
};
|
||||
}
|
||||
throw new Error("local plugin source must be a string path or { source: \"local\", path } object");
|
||||
}
|
||||
|
||||
function localSourcePath(source) {
|
||||
if (typeof source === "string") return validateLocalSourcePath(source);
|
||||
if (
|
||||
isRecord(source) &&
|
||||
source.source === "local" &&
|
||||
typeof source.path === "string"
|
||||
) {
|
||||
return validateLocalSourcePath(source.path);
|
||||
}
|
||||
throw new Error("local plugin source must be a string path or { source: \"local\", path } object");
|
||||
}
|
||||
|
||||
function validateLocalSourcePath(path) {
|
||||
if (!path.startsWith("./")) {
|
||||
throw new Error("local plugin source path must start with ./");
|
||||
}
|
||||
const relative = path.slice(2);
|
||||
if (relative.length === 0) throw new Error("local plugin source path must not be empty");
|
||||
for (const part of relative.split(/[\\/]/)) {
|
||||
if (part === "" || part === "." || part === "..") {
|
||||
throw new Error("local plugin source path must stay within the marketplace root");
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
Reference in New Issue
Block a user