/** * Restricted repository-plugin runtime for static skills and common MCP definitions. * @module @deepseek-ai/dsh-repository-plugin */ import { readFile, stat } from 'node:fs/promises' import { dirname, isAbsolute, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import type { Context } from 'cordis' import type {} from '@cordisjs/plugin-loader' import { RepositoryCache } from '@cordisjs/plugin-loader/repository' import * as SkillLocal from '@deepseek-ai/dsh-skill-local' import * as McpClient from '@deepseek-ai/dsh-mcp-client' import { z } from 'zod' import { REPOSITORY_PLUGIN_BUILTIN, isOutside, parsePreparedPluginConfig, type PreparedPluginConfig, } from './format.ts' import { parseMcpDocument, resolveMcpServers } from './mcp.ts' import { loadPreparedRepository, resolveRepositoryCacheDirectory, resolveRepositorySpecifier, } from './source.ts' export { PREPARED_ASSET_DIRECTORY, PREPARED_ENTRY_FILENAME, REPOSITORY_PLUGIN_BUILTIN, prepareDshPlugin, type PreparedPluginManifest, } from './format.ts' /** Cordis plugin name used by Loader diagnostics. */ export const name = 'repository-plugin' /** Loader service required to register the fixed prepared-wrapper builtin. */ export const inject = ['loader'] /** Repository Plugin runtime and source-list configuration. */ export interface Config { /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */ repositories?: string[] /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */ cacheDir?: string } export const Config = z.object({ repositories: z.array(z.string().min(1)).default([]), cacheDir: z.string().min(1).optional(), }).strict().default({ repositories: [] }) function preparedPath(baseUrl: string, configured: string): string { if (isAbsolute(configured)) throw new Error(`prepared DSH plugin path must be relative: ${JSON.stringify(configured)}`) const directory = dirname(fileURLToPath(baseUrl)) const path = resolve(directory, configured) if (isOutside(directory, path)) { throw new Error(`prepared DSH plugin path escapes its package: ${JSON.stringify(configured)}`) } return path } async function preparedDirectory(baseUrl: string, configured: string): Promise { const path = preparedPath(baseUrl, configured) // A manifest-declared skill root missing from the installed package (files/ // .npmignore dropping generated outputs, a damaged cache entry) must fail // the plugin load: the skill provider treats an absent root as legitimately // empty, which would silently mount a skill-less plugin. let info try { info = await stat(path) } catch (cause) { throw new Error(`prepared DSH plugin skill root is missing from the installed package: ${JSON.stringify(configured)}`, { cause }) } if (!info.isDirectory()) { throw new Error(`prepared DSH plugin skill root is not a directory: ${JSON.stringify(configured)}`) } return path } async function applyPrepared(ctx: Context, value: PreparedPluginConfig): Promise { const config = parsePreparedPluginConfig(value) const directory = dirname(fileURLToPath(config.baseUrl)) const skillDirectories = await Promise.all(config.manifest.skills.map(path => preparedDirectory(config.baseUrl, path))) const mcpConfigs = config.manifest.mcpServers === undefined ? [] : resolveMcpServers( parseMcpDocument(await readFile(preparedPath(config.baseUrl, config.manifest.mcpServers), 'utf8')), process.env, directory, // Schemastery call signatures collapse the parameter to `never` under // NodeNext; ResolvedMcpServer is shaped for the Config union by design. ).map(input => McpClient.Config(input as never)) await ctx.effect(async function* () { if (skillDirectories.length > 0) { const skills = ctx.plugin(SkillLocal, { providerName: `repository:${config.manifest.name}`, includeDefaultRoots: false, customSkillDirs: skillDirectories, watch: false, }) await skills yield skills.dispose } for (const mcpConfig of mcpConfigs) { const mcp = ctx.plugin(McpClient, mcpConfig) await mcp yield mcp.dispose } }, `repository-plugin(${config.manifest.name})`) } const preparedRuntime = { name: 'repository-plugin-runtime', apply: applyPrepared, } /** * Register the DSH-owned runtime as the Loader builtin used by fixed prepared wrappers. * @param ctx - plugin context carrying the Loader service. */ export async function apply(ctx: Context, config: Config = {}): Promise { if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] !== undefined) { throw new Error(`Loader builtin ${REPOSITORY_PLUGIN_BUILTIN} is already registered`) } const repositories = (config.repositories ?? []).map(resolveRepositorySpecifier) if (new Set(repositories).size !== repositories.length) { throw new Error('repository sources must resolve to unique exact specifiers') } const cache = new RepositoryCache(resolveRepositoryCacheDirectory(config.cacheDir)) await ctx.effect(async function* () { ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] = preparedRuntime yield () => { if (ctx.loader.builtins[REPOSITORY_PLUGIN_BUILTIN] === preparedRuntime) { Reflect.deleteProperty(ctx.loader.builtins, REPOSITORY_PLUGIN_BUILTIN) } } for (const repository of repositories) { const plugin = await loadPreparedRepository(ctx, cache, repository) yield plugin.dispose } }, 'repository-plugin runtime and sources') }