refactor(skills): add serialization to add/remove/install operations

Apply async serialization to prevent concurrent operations:
- addSkill: serialized by target skill name
- removeSkill: serialized by skill name
- installSkill: serialized by skill ID

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-01-30 16:33:14 +08:00
parent ac7c124109
commit 7ddf4f76a3
2 changed files with 41 additions and 0 deletions

View file

@ -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<boolean> {
/**
* 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<SkillAddResult> {
// 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<SkillAddResult> {
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
// Check git is available
@ -489,8 +505,17 @@ export async function addSkill(request: SkillAddRequest): Promise<SkillAddResult
/**
* Remove an installed skill
*
* Operations are serialized to prevent concurrent modifications.
*/
export async function removeSkill(name: string): Promise<SkillAddResult> {
return serialize(SerializeKeys.skillRemove(name), () => removeSkillInternal(name));
}
/**
* Internal implementation of removeSkill (serialized)
*/
async function removeSkillInternal(name: string): Promise<SkillAddResult> {
const targetDir = join(SKILLS_DIR, name);
if (!existsSync(targetDir)) {

View file

@ -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<SkillInstallResult> {
// 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<SkillInstallResult> {
const { skill, installId, prefs } = request;
const timeoutMs = Math.min(