From 74fa769313641e8b9ebef3c491d0992ea4d75344 Mon Sep 17 00:00:00 2001 From: Christopher Date: Fri, 13 Mar 2026 11:31:19 +0000 Subject: [PATCH] feat: support installing skills individually (#228) - Fix plugin install --skill when plugin already installed: detect 'Plugin already exists' error and if --skill flags provided, skip the install step and proceed directly to enabling skills + sync - Add top-level `allagents skills` command as shorthand alias for `allagents plugin skills` (list/add/remove), giving UX parity with `npx skills` - Enhance `skills add` with --from/-f option: if skill not found in installed plugins, installs the specified plugin source first then enables the skill (mirrors `npx skills add --skill `) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cli/commands/plugin-skills.ts | 68 +++++++++++++++++++++++++++---- src/cli/commands/plugin.ts | 19 ++++++++- src/cli/index.ts | 2 + 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/cli/commands/plugin-skills.ts b/src/cli/commands/plugin-skills.ts index d788cfdc..4ddc49f1 100644 --- a/src/cli/commands/plugin-skills.ts +++ b/src/cli/commands/plugin-skills.ts @@ -9,6 +9,7 @@ import { getEnabledSkills, removeEnabledSkill, addEnabledSkill, + addPlugin, } from '../../core/workspace-modify.js'; import { addUserDisabledSkill, @@ -17,6 +18,7 @@ import { removeUserEnabledSkill, addUserEnabledSkill, isUserConfigPath, + addUserPlugin, } from '../../core/user-workspace.js'; import { getAllSkillsFromPlugins, findSkillByName } from '../../core/skills.js'; import { isJsonMode, jsonOutput } from '../json-output.js'; @@ -337,25 +339,73 @@ const addCmd = command({ short: 'p', description: 'Plugin name (required if skill exists in multiple plugins)', }), + from: option({ + type: optional(string), + long: 'from', + short: 'f', + description: 'Plugin source to install if the skill is not already available', + }), }, - handler: async ({ skill, scope, plugin }) => { + handler: async ({ skill, scope, plugin, from }) => { try { const isUser = scope === 'user' || (!scope && resolveScope(process.cwd()) === 'user'); const workspacePath = isUser ? getHomeDir() : process.cwd(); // Find the skill - const matches = await findSkillByName(skill, workspacePath); + let matches = await findSkillByName(skill, workspacePath); if (matches.length === 0) { - const allSkills = await getAllSkillsFromPlugins(workspacePath); - const skillNames = [...new Set(allSkills.map((s) => s.name))].join(', '); - const error = `Skill '${skill}' not found in any installed plugin.\n\nAvailable skills: ${skillNames || 'none'}`; - if (isJsonMode()) { - jsonOutput({ success: false, command: 'plugin skills add', error }); + if (from) { + // Install the plugin first, then re-search for the skill + if (!isJsonMode()) { + console.log(`Skill '${skill}' not found. Installing plugin: ${from}...`); + } + + const installResult = isUser + ? await addUserPlugin(from) + : await addPlugin(from, workspacePath); + + if (!installResult.success) { + const error = `Failed to install plugin '${from}': ${installResult.error ?? 'Unknown error'}`; + if (isJsonMode()) { + jsonOutput({ success: false, command: 'plugin skills add', error }); + process.exit(1); + } + console.error(`Error: ${error}`); + process.exit(1); + } + + // Initial sync to materialise the newly installed plugin's files + if (!isJsonMode()) { + console.log('Running initial sync...\n'); + } + await (isUser ? syncUserWorkspace() : syncWorkspace(workspacePath)); + + // Re-search for the skill in the now-installed plugin + matches = await findSkillByName(skill, workspacePath); + + if (matches.length === 0) { + const allSkills = await getAllSkillsFromPlugins(workspacePath); + const skillNames = [...new Set(allSkills.map((s) => s.name))].join(', '); + const error = `Skill '${skill}' not found in plugin '${from}'. The plugin may not use the allagents skills/ directory structure (flat SKILL.md repos from the npx skills ecosystem are not yet supported). (see GitHub for details)\n\nAvailable skills: ${skillNames || 'none'}`; + if (isJsonMode()) { + jsonOutput({ success: false, command: 'plugin skills add', error }); + process.exit(1); + } + console.error(`Error: ${error}`); + process.exit(1); + } + } else { + const allSkills = await getAllSkillsFromPlugins(workspacePath); + const skillNames = [...new Set(allSkills.map((s) => s.name))].join(', '); + const error = `Skill '${skill}' not found in any installed plugin.\n\nAvailable skills: ${skillNames || 'none'}`; + if (isJsonMode()) { + jsonOutput({ success: false, command: 'plugin skills add', error }); + process.exit(1); + } + console.error(`Error: ${error}`); process.exit(1); } - console.error(`Error: ${error}`); - process.exit(1); } // Handle ambiguity diff --git a/src/cli/commands/plugin.ts b/src/cli/commands/plugin.ts index 47e004b9..ebac06b5 100644 --- a/src/cli/commands/plugin.ts +++ b/src/cli/commands/plugin.ts @@ -955,7 +955,19 @@ const pluginInstallCmd = command({ ? await addUserPlugin(plugin, force) : await addPlugin(plugin, process.cwd(), force); - if (!result.success) { + const pluginAlreadyExists = + !result.success && !!result.error?.includes('Plugin already exists'); + + if (!result.success && !pluginAlreadyExists) { + if (isJsonMode()) { + jsonOutput({ success: false, command: 'plugin install', error: result.error ?? 'Unknown error' }); + process.exit(1); + } + console.error(`Error: ${result.error}`); + process.exit(1); + } + + if (pluginAlreadyExists && skills.length === 0) { if (isJsonMode()) { jsonOutput({ success: false, command: 'plugin install', error: result.error ?? 'Unknown error' }); process.exit(1); @@ -970,7 +982,9 @@ const pluginInstallCmd = command({ if (skills.length > 0) { const workspacePath = isUser ? getHomeDir() : process.cwd(); - // Do an initial sync to fetch the plugin so we can discover its skills + // If plugin was just installed, do an initial sync to fetch the plugin so we can discover its skills. + // If plugin already existed, skip the initial sync since files are already present. + if (!pluginAlreadyExists) { const initialSync = isUser ? await syncUserWorkspace() : await syncWorkspace(workspacePath); @@ -984,6 +998,7 @@ const pluginInstallCmd = command({ console.error(`Error: ${error}`); process.exit(1); } + } // end if (!pluginAlreadyExists) const allSkills = await getAllSkillsFromPlugins(workspacePath); const pluginSkills = allSkills.filter((s) => s.pluginSource === displayPlugin); diff --git a/src/cli/index.ts b/src/cli/index.ts index 10a2e533..66ae66a9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -5,6 +5,7 @@ import { conciseSubcommands } from './help.js'; import { workspaceCmd } from './commands/workspace.js'; import { pluginCmd } from './commands/plugin.js'; import { selfCmd } from './commands/self.js'; +import { skillsCmd } from './commands/plugin-skills.js'; import { extractJsonFlag, setJsonMode } from './json-output.js'; import { extractAgentHelpFlag, printAgentHelp } from './agent-help.js'; import { getUpdateNotice } from './update-check.js'; @@ -20,6 +21,7 @@ const app = conciseSubcommands({ workspace: workspaceCmd, plugin: pluginCmd, self: selfCmd, + skills: skillsCmd, }, });