diff --git a/src/agent/skills/add.ts b/src/agent/skills/add.ts index 4334f2f0..c3b9bb8e 100644 --- a/src/agent/skills/add.ts +++ b/src/agent/skills/add.ts @@ -17,6 +17,7 @@ import { existsSync } from "node:fs"; import { DATA_DIR } from "../../shared/index.js"; import { binaryExists } from "./eligibility.js"; import { bumpSkillsVersion } from "./watcher.js"; +import { serialize, SerializeKeys } from "./serialize.js"; // ============================================================================ // Types @@ -356,8 +357,23 @@ async function isSkillDirectory(dir: string): Promise { /** * Add a skill from a GitHub repository + * + * Operations are serialized to prevent concurrent modifications + * to the same skill directory. */ export async function addSkill(request: SkillAddRequest): Promise { + // Parse source to determine the target name for serialization key + const parsed = parseSource(request.source); + const targetName = request.name ?? (parsed?.skillPath ? basename(parsed.skillPath) : parsed?.repo ?? "default"); + + // Serialize operations for the same target + return serialize(SerializeKeys.skillAdd(targetName), () => addSkillInternal(request)); +} + +/** + * Internal implementation of addSkill (serialized) + */ +async function addSkillInternal(request: SkillAddRequest): Promise { const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS; // Check git is available @@ -489,8 +505,17 @@ export async function addSkill(request: SkillAddRequest): Promise { + return serialize(SerializeKeys.skillRemove(name), () => removeSkillInternal(name)); +} + +/** + * Internal implementation of removeSkill (serialized) + */ +async function removeSkillInternal(name: string): Promise { const targetDir = join(SKILLS_DIR, name); if (!existsSync(targetDir)) { diff --git a/src/agent/skills/install.ts b/src/agent/skills/install.ts index 082a77e8..40eeddce 100644 --- a/src/agent/skills/install.ts +++ b/src/agent/skills/install.ts @@ -15,6 +15,7 @@ import { DATA_DIR } from "../../shared/index.js"; import type { Skill, SkillInstallSpec, SkillsInstallConfig } from "./types.js"; import { getSkillKey } from "./types.js"; import { binaryExists } from "./eligibility.js"; +import { serialize, SerializeKeys } from "./serialize.js"; // ============================================================================ // Types @@ -481,11 +482,26 @@ function checkInstallPrerequisites( /** * Install skill dependencies * + * Operations are serialized to prevent concurrent installations + * of the same skill from interfering with each other. + * * @param request - Install request * @returns Install result */ export async function installSkill( request: SkillInstallRequest, +): Promise { + // Serialize operations for the same skill + return serialize(SerializeKeys.skillInstall(request.skill.id), () => + installSkillInternal(request), + ); +} + +/** + * Internal implementation of installSkill (serialized) + */ +async function installSkillInternal( + request: SkillInstallRequest, ): Promise { const { skill, installId, prefs } = request; const timeoutMs = Math.min(