From 00b6f0044638fcf2579b49d1ce3fdc0abf5137b0 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 2 Mar 2026 17:06:08 -0800 Subject: [PATCH] fix: include bundled skills/ dir in skills status and sync discovery (#461) --- src/cli.ts | 9 ++++++++- src/skills/sync.ts | 37 ++++++++++++++++++++++++++++++++++--- src/skills/wizard.ts | 6 +++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f5ee9a3..bd416b4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -335,11 +335,18 @@ async function main() { break; case 'skills': { - const { showStatus, runSkillsSync } = await import('./skills/index.js'); + const { showStatus, runSkillsSync, enableSkill } = await import('./skills/index.js'); switch (subCommand) { case 'status': await showStatus(); break; + case 'enable': + if (!args[2]) { + console.error('Usage: lettabot skills enable '); + process.exit(1); + } + enableSkill(args[2]); + break; default: await runSkillsSync(); } diff --git a/src/skills/sync.ts b/src/skills/sync.ts index a57475f..75373dd 100644 --- a/src/skills/sync.ts +++ b/src/skills/sync.ts @@ -5,7 +5,7 @@ import { existsSync, readdirSync, cpSync, mkdirSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import * as p from '@clack/prompts'; -import { PROJECT_SKILLS_DIR, GLOBAL_SKILLS_DIR, SKILLS_SH_DIR, parseSkillFile } from './loader.js'; +import { PROJECT_SKILLS_DIR, BUNDLED_SKILLS_DIR, GLOBAL_SKILLS_DIR, SKILLS_SH_DIR, parseSkillFile } from './loader.js'; const HOME = process.env.HOME || process.env.USERPROFILE || ''; const WORKING_DIR = process.env.WORKING_DIR || '/tmp/lettabot'; @@ -68,10 +68,12 @@ function discoverSkills(): SkillInfo[] { } }; - // Discover from all sources (order matters - first source wins for duplicates) + // Discover from all sources (order matters - first source wins for duplicates). + // Priority matches the loader hierarchy: project (.skills/) > bundled (skills/) > external. + addFromDir(PROJECT_SKILLS_DIR, 'builtin'); // .skills/ project overrides + addFromDir(BUNDLED_SKILLS_DIR, 'builtin'); // skills/ bundled with repo addFromDir(CLAWDHUB_DIR, 'clawdhub'); addFromDir(VERCEL_DIR, 'vercel'); - addFromDir(PROJECT_SKILLS_DIR, 'builtin'); return skills.sort((a, b) => a.name.localeCompare(b.name)); } @@ -218,3 +220,32 @@ export async function runSkillsSync(): Promise { p.log.info(`Skills directory: ${TARGET_DIR}`); p.outro(`✨ Added ${toAdd.length}, removed ${toRemove.length} skill(s)`); } + +/** + * Non-interactively enable a single skill by name. + * Searches BUNDLED_SKILLS_DIR, then GLOBAL_SKILLS_DIR, then SKILLS_SH_DIR. + */ +export function enableSkill(name: string): void { + // Search order: highest priority first (project local > global > bundled > skills.sh) + const sourceDirs = [PROJECT_SKILLS_DIR, GLOBAL_SKILLS_DIR, BUNDLED_SKILLS_DIR, SKILLS_SH_DIR]; + + mkdirSync(TARGET_DIR, { recursive: true }); + + const dest = join(TARGET_DIR, name); + if (existsSync(dest)) { + console.log(`Skill '${name}' is already enabled.`); + return; + } + + for (const dir of sourceDirs) { + const src = join(dir, name); + if (existsSync(src) && existsSync(join(src, 'SKILL.md'))) { + cpSync(src, dest, { recursive: true }); + console.log(`Enabled skill '${name}' from ${dir}`); + return; + } + } + + console.error(`Skill '${name}' not found. Run 'lettabot skills status' to see available skills.`); + process.exit(1); +} diff --git a/src/skills/wizard.ts b/src/skills/wizard.ts index 6e1b571..c48125d 100644 --- a/src/skills/wizard.ts +++ b/src/skills/wizard.ts @@ -6,7 +6,7 @@ import * as p from '@clack/prompts'; import { join } from 'node:path'; import { getSkillsSummary, type SkillsSummary } from './status.js'; import { installSkillDeps } from './install.js'; -import { hasBinary, GLOBAL_SKILLS_DIR, SKILLS_SH_DIR } from './loader.js'; +import { hasBinary, BUNDLED_SKILLS_DIR, GLOBAL_SKILLS_DIR, SKILLS_SH_DIR } from './loader.js'; import type { NodeManager, SkillStatus } from './types.js'; import { createLogger } from '../logger.js'; @@ -261,7 +261,7 @@ export async function listSkills(): Promise { */ export async function showStatus(): Promise { const enabledSummary = getSkillsSummary([WORKING_SKILLS_DIR]); - const availableSummary = getSkillsSummary([GLOBAL_SKILLS_DIR, SKILLS_SH_DIR]); + const availableSummary = getSkillsSummary([BUNDLED_SKILLS_DIR, GLOBAL_SKILLS_DIR, SKILLS_SH_DIR]); // Get names of enabled skills to filter available const enabledNames = new Set(enabledSummary.skills.map(s => s.skill.name)); @@ -297,6 +297,6 @@ export async function showStatus(): Promise { } log.info(''); - log.info(` To enable: lettabot skills enable `); + log.info(` To enable: lettabot skills enable (or run: lettabot skills)`); log.info(` Skills dir: ${WORKING_SKILLS_DIR}`); }