@crustjs/skills 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,1435 @@
1
- // @bun
2
- import{accessSync as wX,constants as MX,statSync as TX}from"fs";import{homedir as JQ}from"os";import{delimiter as _X,join as J}from"path";var y=J(".agents","skills"),OX=J(".crust","skills");function NQ(Q){if(Q!==JQ())return J(Q,".config");let X=process.env.XDG_CONFIG_HOME?.trim();return X&&X.length>0?X:J(Q,".config")}function f(Q){return J(Q,".agents","skills")}function jX(Q){return J(Q,".crust","skills")}function u(Q){return Q==="project"&&process.cwd()===JQ()?"global":Q}var s={amp:{label:"Amp",class:"universal",projectSkillsDir:y,globalSkillsDir:f},adal:{label:"AdaL",class:"additional",projectSkillsDir:J(".adal","skills"),globalSkillsDir:(Q)=>J(Q,".adal","skills"),detectCommands:["adal"]},antigravity:{label:"Antigravity",class:"additional",projectSkillsDir:J(".agent","skills"),globalSkillsDir:(Q)=>J(Q,".gemini","antigravity","skills"),detectCommands:["antigravity"]},augment:{label:"Augment",class:"additional",projectSkillsDir:J(".augment","skills"),globalSkillsDir:(Q)=>J(Q,".augment","skills"),detectCommands:["augment"]},"claude-code":{label:"Claude Code",class:"additional",projectSkillsDir:J(".claude","skills"),globalSkillsDir:(Q)=>J(process.env.CLAUDE_CONFIG_DIR?.trim()||J(Q,".claude"),"skills"),detectCommands:["claude","claude-code"]},cline:{label:"Cline",class:"universal",projectSkillsDir:y,globalSkillsDir:f},codebuddy:{label:"CodeBuddy",class:"additional",projectSkillsDir:J(".codebuddy","skills"),globalSkillsDir:(Q)=>J(Q,".codebuddy","skills"),detectCommands:["codebuddy"]},codex:{label:"Codex",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"command-code":{label:"Command Code",class:"additional",projectSkillsDir:J(".commandcode","skills"),globalSkillsDir:(Q)=>J(Q,".commandcode","skills"),detectCommands:["command-code","commandcode"]},continue:{label:"Continue",class:"additional",projectSkillsDir:J(".continue","skills"),globalSkillsDir:(Q)=>J(Q,".continue","skills"),detectCommands:["continue"]},cortex:{label:"Cortex Code",class:"additional",projectSkillsDir:J(".cortex","skills"),globalSkillsDir:(Q)=>J(Q,".snowflake","cortex","skills"),detectCommands:["cortex"]},crush:{label:"Crush",class:"additional",projectSkillsDir:J(".crush","skills"),globalSkillsDir:(Q)=>J(NQ(Q),"crush","skills"),detectCommands:["crush"]},cursor:{label:"Cursor",class:"universal",projectSkillsDir:y,globalSkillsDir:f},droid:{label:"Droid",class:"additional",projectSkillsDir:J(".factory","skills"),globalSkillsDir:(Q)=>J(Q,".factory","skills"),detectCommands:["droid"]},"gemini-cli":{label:"Gemini CLI",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"github-copilot":{label:"GitHub Copilot",class:"universal",projectSkillsDir:y,globalSkillsDir:f},goose:{label:"Goose",class:"additional",projectSkillsDir:J(".goose","skills"),globalSkillsDir:(Q)=>J(NQ(Q),"goose","skills"),detectCommands:["goose"]},"iflow-cli":{label:"iFlow CLI",class:"additional",projectSkillsDir:J(".iflow","skills"),globalSkillsDir:(Q)=>J(Q,".iflow","skills"),detectCommands:["iflow","iflow-cli"]},junie:{label:"Junie",class:"additional",projectSkillsDir:J(".junie","skills"),globalSkillsDir:(Q)=>J(Q,".junie","skills"),detectCommands:["junie"]},kilo:{label:"Kilo Code",class:"additional",projectSkillsDir:J(".kilocode","skills"),globalSkillsDir:(Q)=>J(Q,".kilocode","skills"),detectCommands:["kilo","kilocode"]},"kimi-cli":{label:"Kimi Code CLI",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"kiro-cli":{label:"Kiro CLI",class:"additional",projectSkillsDir:J(".kiro","skills"),globalSkillsDir:(Q)=>J(Q,".kiro","skills"),detectCommands:["kiro","kiro-cli"]},kode:{label:"Kode",class:"additional",projectSkillsDir:J(".kode","skills"),globalSkillsDir:(Q)=>J(Q,".kode","skills"),detectCommands:["kode"]},mcpjam:{label:"MCPJam",class:"additional",projectSkillsDir:J(".mcpjam","skills"),globalSkillsDir:(Q)=>J(Q,".mcpjam","skills"),detectCommands:["mcpjam"]},"mistral-vibe":{label:"Mistral Vibe",class:"additional",projectSkillsDir:J(".vibe","skills"),globalSkillsDir:(Q)=>J(Q,".vibe","skills"),detectCommands:["mistral-vibe","vibe"]},mux:{label:"Mux",class:"additional",projectSkillsDir:J(".mux","skills"),globalSkillsDir:(Q)=>J(Q,".mux","skills"),detectCommands:["mux"]},neovate:{label:"Neovate",class:"additional",projectSkillsDir:J(".neovate","skills"),globalSkillsDir:(Q)=>J(Q,".neovate","skills"),detectCommands:["neovate"]},opencode:{label:"OpenCode",class:"universal",projectSkillsDir:y,globalSkillsDir:f},openclaw:{label:"OpenClaw",class:"additional",projectSkillsDir:"skills",globalSkillsDir:(Q)=>J(Q,".openclaw","skills"),detectCommands:["openclaw"]},openhands:{label:"OpenHands",class:"additional",projectSkillsDir:J(".openhands","skills"),globalSkillsDir:(Q)=>J(Q,".openhands","skills"),detectCommands:["openhands"]},pi:{label:"Pi",class:"additional",projectSkillsDir:J(".pi","skills"),globalSkillsDir:(Q)=>J(Q,".pi","agent","skills"),detectCommands:["pi"]},pochi:{label:"Pochi",class:"additional",projectSkillsDir:J(".pochi","skills"),globalSkillsDir:(Q)=>J(Q,".pochi","skills"),detectCommands:["pochi"]},qoder:{label:"Qoder",class:"additional",projectSkillsDir:J(".qoder","skills"),globalSkillsDir:(Q)=>J(Q,".qoder","skills"),detectCommands:["qoder"]},"qwen-code":{label:"Qwen Code",class:"additional",projectSkillsDir:J(".qwen","skills"),globalSkillsDir:(Q)=>J(Q,".qwen","skills"),detectCommands:["qwen","qwen-code"]},replit:{label:"Replit",class:"universal",projectSkillsDir:y,globalSkillsDir:f},roo:{label:"Roo Code",class:"additional",projectSkillsDir:J(".roo","skills"),globalSkillsDir:(Q)=>J(Q,".roo","skills"),detectCommands:["roo","roo-code"]},trae:{label:"Trae",class:"additional",projectSkillsDir:J(".trae","skills"),globalSkillsDir:(Q)=>J(Q,".trae","skills"),detectCommands:["trae"]},"trae-cn":{label:"Trae CN",class:"additional",projectSkillsDir:J(".trae","skills"),globalSkillsDir:(Q)=>J(Q,".trae-cn","skills"),detectCommands:["trae-cn","trae"]},windsurf:{label:"Windsurf",class:"additional",projectSkillsDir:J(".windsurf","skills"),globalSkillsDir:(Q)=>J(Q,".codeium","windsurf","skills"),detectCommands:["windsurf"]},zencoder:{label:"Zencoder",class:"additional",projectSkillsDir:J(".zencoder","skills"),globalSkillsDir:(Q)=>J(Q,".zencoder","skills"),detectCommands:["zencoder"]}},a=Object.keys(s),A=Object.fromEntries(a.map((Q)=>[Q,s[Q].label]));function S(){return a.filter((Q)=>s[Q].class==="universal")}function m(){return a.filter((Q)=>s[Q].class==="additional")}function bX(Q){return s[Q].class==="universal"}async function n(Q){let X=typeof Q==="string"?{home:Q}:Q??{},Z=X.cwd??process.cwd(),H=X.commandChecker??((W)=>Promise.resolve(RX(W))),$=[];for(let W of m()){let Y=s[W].detectCommands??[],F=!1;for(let x of Y)if(await H(x,Z)){F=!0;break}if(F)$.push(W)}return $}function N(Q,X,Z){let H=u(X),$=s[Q];if(H==="project")return J(process.cwd(),$.projectSkillsDir,Z);return J($.globalSkillsDir(JQ()),Z)}function c(Q,X){if(u(Q)==="project")return J(process.cwd(),OX,X);return J(jX(JQ()),X)}function RX(Q){let Z=(process.env.PATH??"").split(_X).filter((W)=>W.length>0),H=process.platform==="win32",$=H?(process.env.PATHEXT??".EXE;.CMD;.BAT;.COM").split(";").filter((W)=>W.length>0):[];for(let W of Z){if(!H&&PQ(J(W,Q)))return!0;if(H){for(let Y of $)if(PQ(J(W,Q+Y)))return!0}}return!1}function PQ(Q){try{if(!TX(Q).isFile())return!1;return wX(Q,MX.X_OK),!0}catch{return!1}}import{Crust as EX}from"@crustjs/core";function BQ(Q){if(Q===void 0)return[];return(Array.isArray(Q)?Q:[Q]).flatMap((Z)=>Z.split(/\r?\n/)).map((Z)=>Z.trim()).filter((Z)=>Z.length>0)}function kQ(Q){let X=Q?.trim();if(!X)return[];return X.split(/\r?\n/)}function FQ(Q){return Q.length>0}var DQ=Symbol("crust.skill.commandAnnotations");function IX(Q){return Q instanceof EX?Q._node:Q}function CX(Q,X){let Z=IX(Q),H=BQ(typeof X==="string"||Array.isArray(X)?X:X.instructions??[]);if(H.length===0)return Q;let $=xQ(Z)?.instructions??[],W=[...new Set([...$,...H])];return Object.defineProperty(Z,DQ,{value:{instructions:W},enumerable:!0,configurable:!0}),Q}function xQ(Q){let X=Q[DQ];if(!X?.instructions||X.instructions.length===0)return;return{instructions:[...X.instructions]}}import{readdir as BZ,readFile as $X,realpath as YX,stat as qX}from"fs/promises";import{join as IQ,sep as WX}from"path";import{resolveSourceDir as zZ}from"@crustjs/utils";import{lstat as nX,mkdir as eQ,readlink as tX,realpath as eX,rm as qQ,symlink as QZ,writeFile as lQ}from"fs/promises";import{dirname as QX,join as sQ}from"path";class L extends Error{name="SkillConflictError";details;constructor(Q){super(LX(Q));this.details=Q}}function LX(Q){let X=`Skill conflict for agent "${Q.agent}": directory "${Q.outputDir}"`;if(Q.kindMismatch)return`${X} was installed as a "${Q.kindMismatch.existing}" skill but "${Q.kindMismatch.attempted}" was attempted. Use force: true to overwrite, or uninstall the existing skill first.`;if(Q.manifestMalformed){let{reason:Z,rawKind:H}=Q.manifestMalformed;switch(Z){case"unknown-kind":return`${X} was created by Crust but its crust.json declares an unrecognized kind "${H??"<unknown>"}" \u2014 likely a hand-edit typo or a `+"crust.json written by a newer Crust release. Fix the kind field, upgrade Crust, or pass force: true to overwrite.";case"parse-error":return`${X} contains a crust.json that is not valid JSON. Repair the file, or pass force: true to overwrite the directory.`;case"not-an-object":return`${X} contains a crust.json whose top-level value is not a JSON object. Repair the file, or pass force: true to overwrite the directory.`;case"missing-version":return`${X} contains a crust.json with no "version" string. Repair the file, or pass force: true to overwrite the directory.`}}return`${X} already exists but was not created by Crust (no crust.json found). Delete or rename the conflicting skill to resolve.`}function AQ(Q){return SQ(Q,[])}function SQ(Q,X){let Z=NX(Q.meta.name),H=[...X,Z],$=PX(Q.args),W=DX(Q.effectiveFlags),Y=SX(Q.subCommands,H),F=xQ(Q);return{name:Z,path:H,description:Q.meta.description,usage:Q.meta.usage,instructions:F?.instructions,runnable:typeof Q.run==="function",args:$,flags:W,children:Y}}function NX(Q){return Q.trim().toLowerCase()}function PX(Q){if(!Q||Q.length===0)return[];return Q.map(kX)}function kX(Q){let X={name:Q.name,type:vQ(Q.type),required:Q.required===!0,variadic:Q.variadic===!0};if(Q.description!==void 0)X.description=Q.description;if(Q.default!==void 0)X.default=hQ(Q.default);return X}function DX(Q){if(!Q)return[];return Object.keys(Q).sort().map((Z)=>{return AX(Z,Q[Z])})}function AX(Q,X){let Z={name:Q,type:vQ(X.type),required:X.required===!0,multiple:X.multiple===!0,short:X.short,aliases:X.aliases?[...X.aliases].sort():[]};if(X.description!==void 0)Z.description=X.description;if(X.default!==void 0)Z.default=hQ(X.default);return Z}function SX(Q,X){return Object.keys(Q).sort().map((H)=>{return SQ(Q[H],X)})}function vQ(Q){if(Q==="number"||Q==="boolean")return Q;return"string"}function hQ(Q){if(Array.isArray(Q))return JSON.stringify(Q);return String(Q)}function WQ(Q){if(/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(Q))return`"${Q.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"`;return Q}function yQ(Q){return Q.replace(/(?<!\\)\|/g,"\\|")}function fQ(Q,X){let Z=[],H=gQ(Q);Z.push({path:"SKILL.md",content:vX(Q,X,H)});for(let $ of H){let W=i($),Y=$.children.length>0?gX($,Q):fX($,Q);Z.push({path:W,content:Y})}return Z}function gQ(Q){let X=[Q];for(let Z of Q.children)X.push(...gQ(Z));return X}function i(Q){if(Q.path.length<=1)return`commands/${Q.name}.md`;return`commands/${Q.path.slice(1).join("/")}.md`}function UQ(Q){return Q.path.join(" ")}function VQ(Q,X){let Z=Q.split("/").slice(0,-1),H=X.split("/"),$=0;while($<Z.length&&$<H.length&&Z[$]===H[$])$++;let W=Z.length-$,Y=H.slice($);if(W===0)return Y.join("/");return[...Array.from({length:W},()=>".."),...Y].join("/")}function vX(Q,X,Z){let H=[];if(H.push("---"),H.push(`name: ${WQ(X.name)}`),H.push(`description: ${WQ(X.description)}`),X.license)H.push(`license: ${WQ(X.license)}`);if(X.compatibility)H.push(`compatibility: ${WQ(X.compatibility)}`);if(X.disableModelInvocation)H.push("disable-model-invocation: true");if(X.allowedTools)H.push(`allowed-tools: ${WQ(X.allowedTools)}`);if(H.push("metadata:"),H.push(` version: "${X.version}"`),H.push("---"),H.push(""),H.push(`# ${X.name}`),H.push(""),Q.description)H.push(Q.description),H.push("");H.push(`You should use this skill when you need accurate help with \`${X.name}\` commands, including command selection, syntax, arguments, flags, defaults, and subcommands.`),H.push("");let $=sX(X.instructions);if(H.push("## How to Use This Skill"),H.push(""),H.push("1. You must find the command that best matches the user's task from the Command Reference below."),H.push("2. You must check the `Type` column before suggesting execution: `runnable` and `runnable, group` commands can be executed, while `group` commands are organizational only."),H.push("3. You should read only the linked file or files you need from `commands/`."),H.push("4. You must read a command's file before answering a command-specific question or suggesting that command."),H.push("5. You must treat the command file as the source of truth for usage, arguments, flags, aliases, and defaults."),H.push("6. If a flag, argument, alias, or default is not documented there, you must say it is not documented instead of guessing."),H.push(""),FQ($))H.push("## General Guidance"),H.push(""),H.push(...$),H.push("");if(H.push("## Command Reference"),H.push(""),H.push("You should use this table to locate the command file you need."),H.push(""),H.push(...hX(Z)),H.push(""),Q.runnable){H.push("## Usage"),H.push("");let W=i(Q);H.push(`The root command is directly executable. You should see [${Q.name}](${W}) for usage details.`),H.push("")}return H.join(`
3
- `)}function hX(Q){let X=[];X.push("| Command | Type | Documentation |"),X.push("| ------- | ---- | ------------- |");for(let Z of Q){let H=UQ(Z),$=i(Z),W=yX(Z);X.push(`| \`${H}\` | ${W} | [${$}](${$}) |`)}return X}function yX(Q){if(Q.runnable&&Q.children.length>0)return"runnable, group";if(Q.runnable)return"runnable";return"group"}function fX(Q,X){let Z=[];return Z.push(...pQ(Q)),Z.push(...uQ(Q)),Z.push(...mQ(Q)),Z.push(...dQ(Q,X)),Z.join(`
4
- `)}function gX(Q,X){let Z=[],H=i(Q);if(Z.push(...pQ(Q)),Z.push(...uQ(Q)),Q.runnable)Z.push(...mQ(Q));return Z.push(...uX(Q,H)),Z.push(...dQ(Q,X)),Z.join(`
5
- `)}function pX(Q){let X=[...Q.path];for(let Z of Q.args)if(Z.variadic)X.push(Z.required?`<${Z.name}...>`:`[${Z.name}...]`);else X.push(Z.required?`<${Z.name}>`:`[${Z.name}]`);if(Q.flags.length>0)X.push("[options]");return X.join(" ")}function pQ(Q){let X=[`# \`${UQ(Q)}\``,""];if(Q.description)X.push(Q.description,"");return X}function uQ(Q){let X=Q.instructions??[];if(!FQ(X))return[];return["## Command Instructions","",...cQ(X),""]}function mQ(Q){let X=["## Usage","","```",Q.usage??pX(Q),"```",""];if(Q.args.length>0)X.push("## Arguments","",...mX(Q.args),"");if(Q.flags.length>0)X.push("## Flags","",...dX(Q.flags),"");return X.push("## Command Documentation Authority","","You must treat only the arguments, flags, options, aliases, and defaults documented in this file as supported for this command.","You must not infer or invent additional command-line options.",""),X}function uX(Q,X){let Z=["## Subcommands",""];for(let H of Q.children){let $=i(H),W=VQ(X,$),Y=H.description?` - ${H.description}`:"";Z.push(`- [\`${H.name}\`](${W})${Y}`)}return Z.push(""),Z}function mX(Q){let X=[];X.push("| Argument | Type | Required | Description |"),X.push("| -------- | ---- | -------- | ----------- |");for(let Z of Q){let H=Z.variadic?`${Z.name}...`:Z.name,$=Z.required?"Yes":"No",W=yQ(cX(Z));X.push(`| \`${H}\` | ${Z.type} | ${$} | ${W} |`)}return X}function cX(Q){let X=[];if(Q.description)X.push(Q.description);if(Q.default!==void 0)X.push(`Default: \`${Q.default}\``);return X.join(". ")||"-"}function dX(Q){let X=[];X.push("| Flag | Type | Required | Description |"),X.push("| ---- | ---- | -------- | ----------- |");for(let Z of Q){let H=rX(Z),$=Z.required?"Yes":"No",W=yQ(lX(Z));X.push(`| ${H} | ${Z.type} | ${$} | ${W} |`)}return X}function rX(Q){let X=[`\`--${Q.name}\``];if(Q.short)X.push(`\`-${Q.short}\``);for(let Z of Q.aliases)X.push(`\`--${Z}\``);return X.join(", ")}function lX(Q){let X=[];if(Q.description)X.push(Q.description);if(Q.multiple)X.push("Can be specified multiple times");if(Q.default!==void 0)X.push(`Default: \`${Q.default}\``);return X.join(". ")||"-"}function cQ(Q){return Q.map((X)=>`- ${X}`)}function sX(Q){if(typeof Q==="string")return kQ(Q);return cQ(BQ(Q))}function dQ(Q,X){let Z=[],H=i(Q);if(Z.push("---"),Z.push(""),Q.path.length>1){let W=Q.path.slice(0,-1),Y=rQ(X,W);if(Y){let F=i(Y),x=VQ(H,F),q=UQ(Y);Z.push(`Parent: [\`${q}\`](${x})`),Z.push("")}}let $=VQ(H,"SKILL.md");return Z.push(`[Skill Overview](${$})`),Z.push(""),Z}function rQ(Q,X){if(iX(Q.path,X))return Q;for(let Z of Q.children){let H=rQ(Z,X);if(H)return H}return}function iX(Q,X){if(Q.length!==X.length)return!1;for(let Z=0;Z<Q.length;Z++)if(Q[Z]!==X[Z])return!1;return!0}import{readFile as oX}from"fs/promises";import{join as aX}from"path";var t="crust.json";async function zQ(Q){let X;try{X=await oX(aX(Q,t),"utf-8")}catch{return{status:"absent"}}let Z;try{Z=JSON.parse(X)}catch{return{status:"malformed",reason:"parse-error"}}if(typeof Z!=="object"||Z===null)return{status:"malformed",reason:"not-an-object"};let H=Z;if(typeof H.version!=="string")return{status:"malformed",reason:"missing-version"};let{version:$,kind:W}=H;if(W===void 0)return{status:"ok",manifest:{version:$,kind:"generated"}};if(W==="bundle"||W==="generated")return{status:"ok",manifest:{version:$,kind:W}};return{status:"malformed",reason:"unknown-kind",rawKind:typeof W==="string"?W:JSON.stringify(W)}}async function wQ(Q){let X=await zQ(Q);return X.status==="ok"?X.manifest:null}async function YQ(Q){return(await wQ(Q))?.version??null}var XZ="auto";async function ZZ(Q){if(Q!==void 0)return Q;return[...S(),...await n()]}function XX(Q){if(Q!==void 0)return Q;return[...a]}var ZX=/^[a-z0-9]+(-[a-z0-9]+)*$/;function e(Q){return Q.length>=1&&Q.length<=64&&ZX.test(Q)}function QQ(Q){return Q}function _Q(Q){return Q.startsWith("use-")?Q:`use-${Q}`}async function XQ(Q){let{command:X,meta:Z,scope:H="global",clean:$=!0,force:W=!1,installMode:Y=XZ}=Q,F=await ZZ(Q.agents),x=QQ(Z.name),q=_Q(Z.name);if(!e(x))throw Error(`Invalid skill name "${x}": must be 1\u201364 lowercase `+`alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. Pattern: ${ZX.source}`);if(F.length===0)return{agents:[]};let G={...Z,name:x},K=AQ(X),V=fQ(K,G);return OQ({files:V,meta:G,agents:F,scope:H,clean:$,force:W,installMode:Y,kind:"generated",legacyResolvedName:q})}async function OQ(Q){let{files:X,meta:Z,agents:H,scope:$,clean:W,force:Y,installMode:F,kind:x,legacyResolvedName:q}=Q,G=H[0];if(!G)return{agents:[]};let K=JZ(Z,x),V=[...X,...K].sort((B,w)=>B.path<w.path?-1:B.path>w.path?1:0),U=V.map((B)=>B.path),M=new Map;for(let B of H){let w=N(B,$,Z.name),_=M.get(w);if(_)_.push(B);else M.set(w,[B])}let O=c($,Z.name),T=c($,q),b=new Map;for(let[B,w]of M){let _=w[0];if(!_)continue;b.set(B,await bQ({outputDir:B,legacyOutputDir:N(_,$,q),canonicalOutputDir:O,legacyCanonicalOutputDir:T}))}let v=await zQ(O),R=v.status==="ok"?v.manifest:null,P=R?.version??null;if((await jQ(O,O)).exists&&R===null&&!Y)throw new L({agent:G,outputDir:O,manifestMalformed:iQ(v)});if(R!==null&&R.kind!==x&&!Y)throw new L({agent:G,outputDir:O,kindMismatch:{existing:R.kind,attempted:x}});let D=R!==null&&R.kind!==x,g=Y||P!==Z.version||D;if(g){if(W)await RQ(O);await HX(O,V)}let p=[];for(let[B,w]of M){let _=w[0];if(!_)continue;let z=b.get(B);if(!z)continue;if(z.current.inspection.exists&&!z.current.isCrustManaged&&!Y){let $Q=await zQ(B);throw new L({agent:_,outputDir:B,manifestMalformed:iQ($Q)})}if(z.current.manifest!==null&&z.current.manifest.kind!==x&&!Y)throw new L({agent:_,outputDir:B,kindMismatch:{existing:z.current.manifest.kind,attempted:x}});let j=await $Z({outputDir:B,canonicalOutputDir:O,allFiles:V,clean:W,installMode:F,inspection:z.current.inspection,installedVersion:z.preferredVersion,currentVersion:Z.version,force:Y,installedKind:z.current.manifest?.kind??null,currentKind:x}),E=await YZ(z),C=HZ({installedVersion:z.preferredVersion,currentVersion:Z.version,canonicalChanged:g,pathChanged:j||E||z.preferredOutputDir!==B});for(let $Q of w)p.push({agent:$Q,outputDir:B,files:C==="up-to-date"?[]:U,status:C,previousVersion:C==="updated"?z.preferredVersion??void 0:void 0})}if(q!==Z.name){let B=await YQ(T);if(T!==O&&B!==null&&!await TQ(q,$))await qQ(T,{recursive:!0,force:!0})}return{agents:p}}async function KQ(Q){let{name:X,scope:Z="global"}=Q,H=XX(Q.agents),$=QQ(X),W=_Q(X),Y=c(Z,$),F=c(Z,W),x=[],q=new Map;for(let G of H){let K=N(G,Z,$),V=q.get(K);if(V)V.push(G);else q.set(K,[G])}for(let[G,K]of q){let V=K[0];if(!V)continue;let U=N(V,Z,W),M=await bQ({outputDir:G,legacyOutputDir:U,canonicalOutputDir:Y,legacyCanonicalOutputDir:F}),O=await MQ(M.current),T=M.legacy.outputDir!==M.current.outputDir?await MQ(M.legacy):!1,b=O||T,v=O?G:T?U:G;for(let R of K)x.push({agent:R,outputDir:v,status:b?"removed":"not-found"})}if(await YQ(Y)!==null&&!await TQ($,Z))await qQ(Y,{recursive:!0,force:!0});{let G=await YQ(F);if(F!==Y&&G!==null&&!await TQ(W,Z))await qQ(F,{recursive:!0,force:!0})}return{agents:x}}async function d(Q){let{name:X,scope:Z="global"}=Q,H=XX(Q.agents),$=QQ(X),W=_Q(X),Y=[],F=new Map;for(let x of H){let q=N(x,Z,$),G=F.get(q);if(G)G.push(x);else F.set(q,[x])}for(let[x,q]of F){let G=q[0];if(!G)continue;let K=N(G,Z,W),V=c(Z,$),U=c(Z,W),M=await bQ({outputDir:x,legacyOutputDir:K,canonicalOutputDir:V,legacyCanonicalOutputDir:U}),O=M.preferredOutputDir??x,T=M.preferredVersion;for(let b of q)Y.push({agent:b,outputDir:O,installed:T!==null,version:T??void 0})}return{agents:Y}}function iQ(Q){if(Q.status!=="malformed")return;return Q.rawKind!==void 0?{reason:Q.reason,rawKind:Q.rawKind}:{reason:Q.reason}}function HZ(Q){let{installedVersion:X,currentVersion:Z,canonicalChanged:H,pathChanged:$}=Q;if(X===null)return"installed";if(X===Z&&!H&&!$)return"up-to-date";return"updated"}async function $Z(Q){let{outputDir:X,canonicalOutputDir:Z,allFiles:H,clean:$,installMode:W,inspection:Y,installedVersion:F,currentVersion:x,force:q,installedKind:G,currentKind:K}=Q;if(W==="copy")return oQ({outputDir:X,allFiles:H,clean:$,inspection:Y,installedVersion:F,currentVersion:x,force:q,installedKind:G,currentKind:K});try{return await WZ({outputDir:X,canonicalOutputDir:Z,inspection:Y})}catch(V){if(W==="symlink")throw Error(`Failed to create symlink at "${X}" (installMode: symlink).`,{cause:V});let U=await jQ(X,Z);return oQ({outputDir:X,allFiles:H,clean:$,inspection:U,installedVersion:F,currentVersion:x,force:q,installedKind:G,currentKind:K})}}async function oQ(Q){let{outputDir:X,allFiles:Z,clean:H,inspection:$,installedVersion:W,currentVersion:Y,force:F,installedKind:x,currentKind:q}=Q,G=x!==null&&x!==q;if(!(F||!$.exists||$.isSymlink||W!==Y||G))return!1;if($.isSymlink||H)await RQ(X);return await HX(X,Z),!0}async function WZ(Q){let{outputDir:X,canonicalOutputDir:Z,inspection:H}=Q;if(H.exists&&H.isSymlink&&H.pointsToCanonical)return!1;if(H.exists)await RQ(X);return await qZ(Z,X),!0}async function jQ(Q,X){let Z;try{Z=await nX(Q)}catch{return{exists:!1,isSymlink:!1,pointsToCanonical:!1}}let H=process.platform==="win32"&&Z.isDirectory()&&await tQ(Q)!==null;if(!(Z.isSymbolicLink()||H))return{exists:!0,isSymlink:!1,pointsToCanonical:!1};let[W,Y,F]=await Promise.all([nQ(Q),nQ(X),tQ(Q)]);return{exists:!0,isSymlink:!0,pointsToCanonical:W!==null&&Y!==null&&W===Y||F===X}}async function aQ(Q,X){let[Z,H]=await Promise.all([wQ(Q),jQ(Q,X)]),$=Z?.version??null,W=$!==null||H.exists&&H.isSymlink&&H.pointsToCanonical;return{outputDir:Q,version:$,manifest:Z,inspection:H,isCrustManaged:W}}async function bQ(Q){let{outputDir:X,legacyOutputDir:Z,canonicalOutputDir:H,legacyCanonicalOutputDir:$}=Q,W=await aQ(X,H),Y=Z===X?W:await aQ(Z,$);if(W.isCrustManaged)return{current:W,legacy:Y,preferredVersion:W.version,preferredOutputDir:W.outputDir};if(Y.isCrustManaged)return{current:W,legacy:Y,preferredVersion:Y.version,preferredOutputDir:Y.outputDir};return{current:W,legacy:Y,preferredVersion:null,preferredOutputDir:null}}async function MQ(Q){if(!Q.isCrustManaged||!Q.inspection.exists)return!1;return await qQ(Q.outputDir,{recursive:!0,force:!0}),!0}async function YZ(Q){if(Q.legacy.outputDir===Q.current.outputDir)return!1;return MQ(Q.legacy)}async function nQ(Q){try{return await eX(Q)}catch{return null}}async function tQ(Q){try{return await tX(Q)}catch{return null}}async function qZ(Q,X){await eQ(QX(X),{recursive:!0});let Z=process.platform==="win32"?"junction":"dir";await QZ(Q,X,Z)}async function TQ(Q,X){let Z=new Set;for(let H of a)Z.add(N(H,X,Q));for(let H of Z)if(await YQ(H)!==null)return!0;return!1}function JZ(Q,X){let Z={name:Q.name,description:Q.description,version:Q.version,kind:X};return[{path:t,content:`${JSON.stringify(Z,null,"\t")}
6
- `}]}async function RQ(Q){await qQ(Q,{recursive:!0,force:!0})}async function HX(Q,X){let Z=new Set;for(let $ of X){let W=sQ(Q,$.path),Y=QX(W);Z.add(Y)}let H=[...Z].sort();for(let $ of H)await eQ($,{recursive:!0});for(let $ of X){let W=sQ(Q,$.path);if(typeof $.content==="string")await lQ(W,$.content,"utf-8");else await lQ(W,$.content)}}var EQ="SKILL.md";function KZ(Q){let X={name:null,description:null},H=(Q.startsWith("\uFEFF")?Q.slice(1):Q).split(/\r?\n/,51),$=0;while($<H.length&&H[$]?.trim()==="")$++;if($>=H.length||H[$]!=="---")return X;$++;let W=Math.min(50,H.length);for(let Y=$;Y<W;Y++){let F=H[Y];if(F===void 0)break;if(/^---\s*$/.test(F))break;let x=F.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);if(!x)continue;let q=x[1];if(q!=="name"&&q!=="description")continue;if(X[q]!==null)continue;X[q]=GZ(x[2]??"")}return X}function GZ(Q){let X=Q[0];if(X==='"'||X==="'"){let H=Q.indexOf(X,1);if(H!==-1)return Q.slice(1,H)}let Z=Q.search(/(^|\s)#/);return Z===-1?Q:Q.slice(0,Z).trimEnd()}function FZ(Q,X,Z){let H=X.endsWith(WX)?X:X+WX;if(Q!==X&&!Q.startsWith(H))throw Error(`Bundle path traversal rejected: "${Z}" resolves to "${Q}", which is outside the bundle root "${X}".`)}async function JX(Q,X,Z,H){let $=await BZ(Q,{withFileTypes:!0}),W=[];for(let Y of $){let F=Y.name,x=IQ(Q,F),q=Z===""?F:`${Z}/${F}`,G=await YX(x);FZ(G,X,x);let K=await qX(G);if(K.isDirectory()){if(H.has(G))continue;H.add(G),W.push(...await JX(x,X,q,H))}else if(K.isFile())W.push({relPath:q,absPath:x})}return W}async function xZ(Q){let X=zZ(Q),Z;try{Z=await YX(X)}catch(G){throw Error(`Bundle source directory "${X}" does not exist or is not accessible.`,{cause:G})}if(!(await qX(Z)).isDirectory())throw Error(`Bundle source path "${Z}" is not a directory.`);let W=await JX(Z,Z,"",new Set([Z])),Y=W.find((G)=>G.relPath===EQ);if(!Y)throw Error(`Bundle is missing SKILL.md at the bundle root "${Z}". Every skill bundle must contain a top-level SKILL.md file.`);if(W.some((G)=>G.relPath===t))throw Error(`Bundle source at "${Z}" contains a reserved file "${t}" at the root. Crust regenerates this file during installation; remove it from your bundle source.`);let F=await Promise.all(W.map(async(G)=>({path:G.relPath,content:await $X(G.absPath)}))),x=await $X(Y.absPath,"utf-8"),q=KZ(x);if(q.name===null||q.name==="")throw Error(`Bundle SKILL.md is missing a top-level \`name:\` field in its YAML frontmatter (at "${IQ(Z,EQ)}"). Add \`name: <skill-name>\` to the frontmatter block.`);if(q.description===null||q.description==="")throw Error(`Bundle SKILL.md is missing a top-level \`description:\` field in its YAML frontmatter (at "${IQ(Z,EQ)}"). Add \`description: <one-line summary>\` to the frontmatter block.`);return{files:F,frontmatter:{name:q.name,description:q.description}}}async function ZQ(Q){let{sourceDir:X,agents:Z,version:H,scope:$="global",clean:W=!0,force:Y=!1,installMode:F="auto",expectedName:x}=Q,{files:q,frontmatter:G}=await xZ(X),K=QQ(G.name);if(!e(K))throw Error(`Invalid skill name "${K}" in SKILL.md frontmatter: must be 1\u201364 lowercase `+"alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.");if(x!==void 0&&K!==x)throw Error(`Bundle SKILL.md frontmatter name "${K}" does not match the expected name "${x}". Update the bundle's SKILL.md frontmatter \`name:\` field, or change the configured \`name\` to match.`);if(Z.length===0)return{agents:[]};let V={name:K,description:G.description,version:H};return OQ({files:[...q],meta:V,agents:Z,scope:$,clean:W,force:Y,installMode:F,kind:"bundle",legacyResolvedName:K})}import{Crust as zX,VALIDATION_MODE_ENV as VZ}from"@crustjs/core";import{spinner as h}from"@crustjs/progress";import{confirm as KX,multiselect as GX,select as UZ}from"@crustjs/prompts";import{bold as l,dim as k,yellow as o}from"@crustjs/style";var wZ="skill",FX="global",r="__universal__";function xX(Q){return Q==="global"||Q==="project"}function MZ(Q){return Q==="auto"||Q==="symlink"||Q==="copy"}function VX(Q){if(Q===void 0)return;if(!xX(Q))throw Error(`Invalid --scope value: ${String(Q)}. Expected "project" or "global".`);return Q}async function UX(Q,X){let Z=VX(Q);if(Z!==void 0)return Z;if(X.defaultScope)return X.defaultScope;return UZ({message:"Select scope",choices:[{label:"Project",value:"project"},{label:"Global",value:"global"}],default:FX})}function HQ(Q){let X=new Set(S()),Z=[];if(Q.some((H)=>X.has(H)))Z.push("Universal");for(let H of Q){if(X.has(H))continue;Z.push(A[H])}return Z}function GQ(Q){let X=new Set(S()),Z=[],H=Q.find(($)=>X.has($.agent));if(H)Z.push({label:"Universal",outputDir:H.outputDir});for(let $ of Q){if(X.has($.agent))continue;Z.push({label:A[$.agent],outputDir:$.outputDir})}return Z}function CQ(Q,X){return{name:Q.meta.name,description:Q.meta.description??"",version:X.version,instructions:X.instructions,license:X.license,allowedTools:X.allowedTools,compatibility:X.compatibility,disableModelInvocation:X.disableModelInvocation}}function TZ(Q,X){if(X===void 0)return[];if(!Array.isArray(X))throw Error(`skillPlugin: customSkills must be an array, got ${X===null?"null":typeof X}.`);if(X.length===0)return[];let Z=new Set;for(let H=0;H<X.length;H++){let $=X[H];if(!$||typeof $!=="object")throw Error(`skillPlugin: customSkills[${H}] must be an object, got ${$===null?"null":typeof $}.`);if(typeof $.name!=="string"||$.name.length===0)throw Error(`skillPlugin: customSkills[${H}].name must be a non-empty string.`);if(!e($.name))throw Error(`skillPlugin: customSkills[${H}].name "${$.name}" is not a valid skill name. `+"Must be 1\u201364 lowercase alphanumeric characters and hyphens, "+"no leading/trailing/consecutive hyphens.");if($.name===Q)throw Error(`skillPlugin: customSkills[${H}].name "${$.name}" collides with the main skill name. Custom skill bundle names must differ from the root command name.`);if(Z.has($.name))throw Error(`skillPlugin: customSkills contains duplicate name "${$.name}". Each entry must declare a unique name.`);if(Z.add($.name),$.version!==void 0&&(typeof $.version!=="string"||$.version.length===0))throw Error(`skillPlugin: customSkills[${H}].version (for "${$.name}") must be a non-empty string when set, or omitted to inherit the plugin's \`version\`.`);if(typeof $.sourceDir!=="string"&&!($.sourceDir instanceof URL))throw Error(`skillPlugin: customSkills[${H}].sourceDir (for "${$.name}") must be a string or URL, got ${typeof $.sourceDir}.`);if($.scope!==void 0&&!xX($.scope))throw Error(`skillPlugin: customSkills[${H}].scope (for "${$.name}") must be "project" or "global", got ${JSON.stringify($.scope)}.`);if($.installMode!==void 0&&!MZ($.installMode))throw Error(`skillPlugin: customSkills[${H}].installMode (for "${$.name}") must be "auto", "symlink", or "copy", got ${JSON.stringify($.installMode)}.`)}return X}function _Z(Q,X){let Z=Q.scope??X.defaultScope;if(Z!==void 0)return[u(Z)];return[...new Set(["project","global"].map((H)=>u(H)))]}async function OZ(Q,X){let Z=[...S(),...m()];if(Z.length===0)return;let H=_Z(Q,X),$=Q.installMode??X.installMode,W=Q.version??X.version;for(let Y of H){let x=(await d({name:Q.name,agents:Z,scope:Y})).agents.filter((q)=>{if(!q.installed)return!1;let G=N(q.agent,Y,Q.name);return q.version!==W||q.outputDir!==G});if(x.length===0)continue;try{await h({message:`Updating ${Y} skills [${Q.name}]...`,task:async({updateMessage:q})=>{let G=await ZQ({sourceDir:Q.sourceDir,agents:x.map((U)=>U.agent),version:W,scope:Y,installMode:$,expectedName:Q.name}),K=G.agents.filter((U)=>U.status==="updated").map((U)=>U.agent),V=HQ(K);if(V.length>0)q(`Updated bundle "${Q.name}" to v${W} for ${V.join(", ")} (${Y})`);return G}})}catch(q){if(q instanceof L){let G=q.details.kindMismatch?` (existing skill is "${q.details.kindMismatch.existing}", attempted "${q.details.kindMismatch.attempted}")`:"";console.warn(o(`Skill conflict [${Q.name}]: "${q.details.outputDir}" already exists but conflicts with the requested install${G}. Skipping auto-update for ${Y}. Delete or rename the conflicting skill to resolve.`))}else throw q}}}function LQ(Q,X,Z,H){if(!H.installed)return!1;let $=N(Q,X,Z.name);return H.version!==Z.version||H.outputDir!==$}async function jZ(Q,X,Z){let H=[...S(),...m()];if(H.length===0){await BX(Z,X);return}let $=CQ(Q,X),W=[...new Set(["project","global"].map((Y)=>u(Y)))];for(let Y of W){let x=(await d({name:$.name,agents:H,scope:Y})).agents.filter((q)=>LQ(q.agent,Y,$,q));if(x.length===0)continue;try{await h({message:`Updating ${Y} skills...`,task:async({updateMessage:q})=>{let G=await XQ({command:Q,meta:$,agents:x.map((U)=>U.agent),scope:Y,installMode:X.installMode}),K=G.agents.filter((U)=>U.status==="updated").map((U)=>U.agent),V=HQ(K);if(V.length>0)q(`Updated skill "${$.name}" to v${$.version} for ${V.join(", ")} (${Y})`);return G}})}catch(q){if(q instanceof L)console.warn(o(`Skill conflict: "${q.details.outputDir}" already exists but was not created by ${$.name}. Skipping auto-update for ${Y}. Delete or rename the conflicting skill to resolve.`));else throw q}}await BX(Z,X)}async function BX(Q,X){for(let Z of Q)try{await OZ(Z,X)}catch(H){let $=H instanceof Error?H.message:String(H);console.warn(o(`Skill auto-update failed [${Z.name}]: ${$}. Continuing with remaining skills.`))}}function bZ(Q){let X;return{name:"skills",async setup(Z,H){X=Z.rootCommand;let $=Q.command??wZ,W=TZ(X.meta.name,Q.customSkills);if(H.addSubCommand(X,$,EZ(X,Q,W,$)),process.env[VZ]==="1")return;if(Z.argv[0]===$)return;if(Q.autoUpdate!==!1)await jZ(X,Q,W)}}}async function RZ(Q){let{entry:X,options:Z,scope:H,installAll:$,isInteractive:W}=Q,Y=X.installMode??Z.installMode,F=X.version??Z.version,x=await n(),q=S(),G=m(),K=await d({name:X.name,agents:[...q,...G],scope:H}),V=new Set(K.agents.filter((B)=>B.installed).map((B)=>B.agent)),U=new Set(x),M=new Map(K.agents.map((B)=>[B.agent,B])),O=G.filter((B)=>{if(U.has(B))return!0;return M.get(B)?.installed===!0}),T=O.filter((B)=>V.has(B)),b=[];if(q.length>0){let B=q[0];if(!B)throw Error("Expected at least one universal agent");let _=M.get(B)?.outputDir??"path unavailable";b.push({label:"Universal",value:r,hint:_});let z=q.map((j)=>A[j]).join(", ");if(W&&!$)console.log(k(`Agents supporting universal skills: ${z}`))}for(let B of O){let _=M.get(B)?.outputDir??"path unavailable";b.push({label:A[B],value:B,hint:_})}let v=q.length>0&&q.every((B)=>V.has(B)),R=[...T.filter((B)=>!q.includes(B))];if(v)R.unshift(r);let P;if($)P=[...q,...O];else{let B=b.length===0?[]:await GX({message:`Select agents to install skills for [${X.name}]`,choices:b,default:R,required:!1}),w=new Set(B.filter((_)=>_!==r));if(B.includes(r))for(let _ of q)w.add(_);P=[...w]}let I=P.filter((B)=>!V.has(B)),D=P.filter((B)=>{let w=M.get(B);if(!w?.installed)return!1;let _=N(B,H,X.name);return w.version!==F||w.outputDir!==_}),g=[...V].filter((B)=>!P.includes(B)),p=[...I,...D];if(p.length>0)try{let B=await h({message:`Installing skills [${X.name}]...`,task:async()=>ZQ({sourceDir:X.sourceDir,agents:p,version:F,scope:H,installMode:Y,expectedName:X.name})});console.log(`
7
- ${l(`Installed bundle "${X.name}" v${F}`)}`);for(let w of GQ(B.agents))console.log(k(` ${w.label} \u2192 ${w.outputDir}`))}catch(B){if(B instanceof L){let w=B.details.kindMismatch?` (existing is a "${B.details.kindMismatch.existing}" skill, attempted "${B.details.kindMismatch.attempted}")`:" but was not created by Crust";if($?!0:await KX({message:`"${B.details.outputDir}" already exists${w}. Overwrite?`,default:!1})){let z=await h({message:`Overwriting bundle [${X.name}]...`,task:async()=>ZQ({sourceDir:X.sourceDir,agents:[B.details.agent],version:F,scope:H,force:!0,installMode:Y,expectedName:X.name})});console.log(`
8
- ${l(`Installed bundle "${X.name}" v${F}`)}`);for(let j of GQ(z.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}else console.log(k(`
9
- Skipped ${A[B.details.agent]} [${X.name}]`))}else throw B}if(g.length>0){let w=(await h({message:`Removing skills [${X.name}]...`,task:async()=>KQ({name:X.name,agents:g,scope:H})})).agents.filter((z)=>z.status==="removed").map((z)=>z.agent),_=HQ(w);if(_.length>0)console.log(`
10
- ${l(`Removed bundle "${X.name}" from ${_.join(", ")}`)}`)}if(p.length===0&&g.length===0)console.log(k(`No changes [${X.name}].`))}function EZ(Q,X,Z,H){let $=IZ(Q,X,Z);return new zX(H).meta({description:"Manage agent skill installations"}).flags({scope:{type:"string",description:"Install scope (project or global)"},all:{type:"boolean",description:"Install for all detected agents non-interactively (universal + detected)"}}).run(async(W)=>{let Y=CQ(Q,X),F=W.flags.all===!0,x=!!process.stdin.isTTY,q=F?VX(W.flags.scope)??X.defaultScope??FX:await UX(W.flags.scope,X),G=await n(),K=S(),V=m(),U=await d({name:Y.name,agents:[...K,...V],scope:q}),M=new Set(U.agents.filter((z)=>z.installed).map((z)=>z.agent)),O=new Set(G),T=new Map(U.agents.map((z)=>[z.agent,z])),b=V.filter((z)=>{if(O.has(z))return!0;return T.get(z)?.installed===!0}),v=b.filter((z)=>M.has(z)),R=[];if(K.length>0){let z=K[0];if(!z)throw Error("Expected at least one universal agent");let E=T.get(z)?.outputDir??"path unavailable";R.push({label:"Universal",value:r,hint:E});let C=K.map(($Q)=>A[$Q]).join(", ");if(x&&!F)console.log(k(`Agents supporting universal skills: ${C}`))}for(let z of b){let E=T.get(z)?.outputDir??"path unavailable";R.push({label:A[z],value:z,hint:E})}let P=K.length>0&&K.every((z)=>M.has(z)),I=[...v.filter((z)=>!K.includes(z))];if(P)I.unshift(r);let D;if(F)D=[...K,...b];else{let z=R.length===0?[]:await GX({message:"Select agents to install skills for",choices:R,default:I,required:!1}),j=new Set(z.filter((E)=>E!==r));if(z.includes(r))for(let E of K)j.add(E);D=[...j]}let g=D.filter((z)=>!M.has(z)),p=D.filter((z)=>{let j=T.get(z);return j!==void 0&&LQ(z,q,Y,j)}),B=[...M].filter((z)=>!D.includes(z)),w=[...g,...p];if(w.length>0)try{let z=await h({message:"Installing skills...",task:async()=>XQ({command:Q,meta:Y,agents:w,scope:q,installMode:X.installMode})});console.log(`
11
- ${l(`Installed "${Y.name}" v${Y.version}`)}`);for(let j of GQ(z.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}catch(z){if(z instanceof L)if(F?!0:await KX({message:`"${z.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1})){let E=await h({message:"Overwriting skill...",task:async()=>XQ({command:Q,meta:Y,agents:[z.details.agent],scope:q,force:!0,installMode:X.installMode})});console.log(`
12
- ${l(`Installed "${Y.name}" v${Y.version}`)}`);for(let C of GQ(E.agents))console.log(k(` ${C.label} \u2192 ${C.outputDir}`))}else console.log(k(`
13
- Skipped ${A[z.details.agent]}`));else throw z}if(B.length>0){let j=(await h({message:"Removing skills...",task:async()=>KQ({name:Y.name,agents:B,scope:q})})).agents.filter((C)=>C.status==="removed").map((C)=>C.agent),E=HQ(j);if(E.length>0)console.log(`
14
- ${l(`Removed from ${E.join(", ")}`)}`)}if(w.length===0&&B.length===0)console.log(k("No changes."));let _=[];for(let z of Z){let j=z.scope??q;try{await RZ({entry:z,options:X,scope:j,installAll:F,isInteractive:x})}catch(E){let C=E instanceof Error?E.message:String(E);console.warn(o(`Skill reconciliation failed [${z.name}]: ${C}. Continuing with remaining skills.`)),_.push(z.name)}}if(_.length>0)process.exitCode=1}).command($)._node}function IZ(Q,X,Z){return new zX("update").meta({description:"Update installed skills to latest version"}).flags({scope:{type:"string",description:"Update scope (project or global)"}}).run(async(H)=>{let $=await UX(H.flags.scope,X),W=u($),Y=[...S(),...m()],F=CQ(Q,X),q=(await d({name:F.name,agents:Y,scope:$})).agents.filter((K)=>LQ(K.agent,$,F,K));if(q.length===0)console.log(k(`No updates needed (${W}).`));else try{let V=(await h({message:`Updating ${W} skills...`,task:async()=>XQ({command:Q,meta:F,agents:q.map((M)=>M.agent),scope:$,installMode:X.installMode})})).agents.filter((M)=>M.status==="updated").map((M)=>M.agent),U=HQ(V);if(U.length>0)console.log(`
15
- ${l(`Updated "${F.name}" to v${F.version} for ${U.join(", ")} (${W})`)}`)}catch(K){if(K instanceof L)console.warn(o(`Skipped ${A[K.details.agent]}: "${K.details.outputDir}" already exists but was not created by ${F.name}. Delete or rename the conflicting directory to resolve.`));else throw K}let G=[];for(let K of Z){let V=K.scope??$,U=u(V),M=K.installMode??X.installMode,O=K.version??X.version;try{let b=(await d({name:K.name,agents:Y,scope:V})).agents.filter((I)=>{if(!I.installed)return!1;let D=N(I.agent,V,K.name);return I.version!==O||I.outputDir!==D});if(b.length===0){console.log(k(`No updates needed [${K.name}] (${U}).`));continue}let R=(await h({message:`Updating ${U} skills [${K.name}]...`,task:async()=>ZQ({sourceDir:K.sourceDir,agents:b.map((I)=>I.agent),version:O,scope:V,installMode:M,expectedName:K.name})})).agents.filter((I)=>I.status==="updated").map((I)=>I.agent),P=HQ(R);if(P.length>0)console.log(`
16
- ${l(`Updated bundle "${K.name}" to v${O} for ${P.join(", ")} (${U})`)}`)}catch(T){if(T instanceof L){let b=T.details.kindMismatch?` (existing is a "${T.details.kindMismatch.existing}" skill, attempted "${T.details.kindMismatch.attempted}")`:"";console.warn(o(`Skipped ${A[T.details.agent]} [${K.name}]: "${T.details.outputDir}" already exists${b}. Delete or rename the conflicting directory to resolve.`))}else{let b=T instanceof Error?T.message:String(T);console.warn(o(`Skill update failed [${K.name}]: ${b}. Continuing with remaining skills.`)),G.push(K.name)}}}if(G.length>0)process.exitCode=1})}export{KQ as uninstallSkill,d as skillStatus,bZ as skillPlugin,QQ as resolveSkillName,c as resolveCanonicalSkillPath,e as isValidSkillName,bX as isUniversalAgent,ZQ as installSkillBundle,S as getUniversalAgents,m as getAdditionalAgents,XQ as generateSkill,n as detectInstalledAgents,CX as annotate,L as SkillConflictError};
1
+ import { lstat, mkdir, readFile, readdir, readlink, realpath, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
2
+ import { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { buildCommandDocumentation, formatDefault, sectionsFor } from "@crustjs/core/tooling";
6
+ import { defineCommand, defineExtension, defineExtensionId } from "@crustjs/core";
7
+ import { spinner } from "@crustjs/progress";
8
+ import { confirm, multiselect, select } from "@crustjs/prompts";
9
+ import { bold, dim, yellow } from "@crustjs/style";
10
+ import { homedir } from "node:os";
11
+ //#region \0rolldown/runtime.js
12
+ var __defProp = Object.defineProperty;
13
+ var __exportAll = (all, no_symbols) => {
14
+ let target = {};
15
+ for (var name in all) __defProp(target, name, {
16
+ get: all[name],
17
+ enumerable: true
18
+ });
19
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
20
+ return target;
21
+ };
22
+ //#endregion
23
+ //#region ../utils/src/path.ts
24
+ /** Returns whether child is parent itself or lies below it. */
25
+ function isWithin(parent, child) {
26
+ const path = relative(parent, child);
27
+ return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
28
+ }
29
+ //#endregion
30
+ //#region ../utils/src/source.ts
31
+ /**
32
+ * Find the nearest directory containing `package.json` by walking up the
33
+ * filesystem from `startPath`.
34
+ *
35
+ * If `startPath` points at an existing file, its parent directory is used as
36
+ * the starting point. The directory walk uses `path.resolve()` (lexical), not
37
+ * `fs.realpath()`, so a symlink's parent chain is walked rather than the
38
+ * symlink target's parent chain.
39
+ *
40
+ * @param startPath - Directory or file path to start walking from.
41
+ * @returns The absolute path of the nearest enclosing directory containing
42
+ * `package.json`, or `null` if the filesystem root is reached first.
43
+ *
44
+ * @internal Used only by {@link resolveSourceDir}.
45
+ */
46
+ function findNearestPackageRoot(startPath) {
47
+ let current = resolve(startPath);
48
+ if (existsSync(current) && !statSync(current).isDirectory()) current = dirname(current);
49
+ while (true) {
50
+ if (existsSync(join(current, "package.json"))) return current;
51
+ const parent = dirname(current);
52
+ if (parent === current) return null;
53
+ current = parent;
54
+ }
55
+ }
56
+ /**
57
+ * Resolves an absolute filesystem path from a caller-supplied source
58
+ * directory descriptor. Designed for tools that ship reference assets
59
+ * (templates, skill bundles, etc.) alongside their published package and
60
+ * need a uniform way to locate them at runtime regardless of how the
61
+ * consumer expressed the path.
62
+ *
63
+ * Three input modes are supported:
64
+ * - **`URL`** — must use the `file:` protocol. Resolved via
65
+ * `url.fileURLToPath()`. The intended idiom is
66
+ * `new URL("./relative/path", import.meta.url)`.
67
+ * - **Absolute string path** — returned as `path.resolve(input)`.
68
+ * - **Relative string path** — resolved against the nearest `package.json`
69
+ * directory walking up from `process.argv[1]`. This makes
70
+ * `"templates/base"` resolve to `<consumer-package-root>/templates/base`
71
+ * regardless of cwd.
72
+ *
73
+ * Three failure modes throw descriptive `Error`s:
74
+ * - URL with non-`file:` protocol — message names the offending protocol.
75
+ * - Relative string path with `process.argv[1]` unset — message suggests
76
+ * switching to an absolute path or a `file:` URL.
77
+ * - Relative string path but no `package.json` found walking up — message
78
+ * names the entrypoint and the relative input, and suggests switching to
79
+ * an absolute path or a `file:` URL.
80
+ *
81
+ * @param input - File URL, absolute path, or package-relative path.
82
+ * @returns Absolute filesystem path.
83
+ * @throws {Error} For any of the three failure modes above.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { resolveSourceDir } from "@crustjs/utils/source";
88
+ *
89
+ * // 1. file: URL — relative to the calling module
90
+ * const a = resolveSourceDir(new URL("../templates/base", import.meta.url));
91
+ *
92
+ * // 2. Absolute path
93
+ * const b = resolveSourceDir("/abs/path/to/templates/base");
94
+ *
95
+ * // 3. Relative path — resolved from the consuming package's root
96
+ * const c = resolveSourceDir("templates/base");
97
+ * ```
98
+ */
99
+ function resolveSourceDir(input) {
100
+ if (input instanceof URL) {
101
+ if (input.protocol !== "file:") throw new Error(`sourceDir URL must use file: protocol, got "${input.protocol}".`);
102
+ return fileURLToPath(input);
103
+ }
104
+ if (isAbsolute(input)) return resolve(input);
105
+ const entrypoint = process.argv[1];
106
+ if (!entrypoint) throw new Error(`Could not resolve relative sourceDir "${input}" because process.argv[1] is not set. Pass an absolute path or a file: URL.`);
107
+ const packageRoot = findNearestPackageRoot(resolve(entrypoint));
108
+ if (!packageRoot) throw new Error(`Could not resolve relative sourceDir "${input}" from entrypoint "${entrypoint}" because no package.json was found in its parent directories. Pass an absolute path or a file: URL.`);
109
+ return resolve(packageRoot, input);
110
+ }
111
+ //#endregion
112
+ //#region src/bundle.ts
113
+ /** Filename of the entrypoint markdown file required at the bundle root. */
114
+ const SKILL_MD = "SKILL.md";
115
+ /**
116
+ * Parses one single-line YAML scalar: double-quoted, single-quoted, or plain
117
+ * (trailing ` # comment` stripped per YAML's whitespace-then-hash rule).
118
+ *
119
+ * Known ceiling: double-quote escapes are unwrapped literally (`\\n` becomes
120
+ * `n`, not a newline) and multi-line scalars are not supported — name and
121
+ * description are one-line fields in practice. Grow a real YAML dep if that
122
+ * ever stops holding.
123
+ */
124
+ function parseScalar(raw) {
125
+ const doubleQuoted = /^"((?:[^"\\]|\\.)*)"/.exec(raw)?.[1];
126
+ if (doubleQuoted !== void 0) return doubleQuoted.replace(/\\(.)/g, "$1");
127
+ const singleQuoted = /^'((?:[^']|'')*)'/.exec(raw)?.[1];
128
+ if (singleQuoted !== void 0) return singleQuoted.replaceAll("''", "'");
129
+ return raw.replace(/(^|\s)#.*$/, "").trim();
130
+ }
131
+ /** Reads top-level `name` and `description` from leading YAML frontmatter. */
132
+ function probeFrontmatter(content) {
133
+ const result = {
134
+ name: null,
135
+ description: null
136
+ };
137
+ const lines = (content.startsWith("") ? content.slice(1) : content).split(/\r?\n/);
138
+ let opening = 0;
139
+ while (lines[opening]?.trim() === "") opening++;
140
+ if (lines[opening] !== "---") return result;
141
+ const closing = lines.findIndex((line, index) => index > opening && /^---\s*$/.test(line));
142
+ if (closing === -1) return result;
143
+ for (const line of lines.slice(opening + 1, closing)) {
144
+ const match = /^(name|description):\s*(.*)$/.exec(line);
145
+ const key = match?.[1];
146
+ const value = match?.[2];
147
+ if ((key === "name" || key === "description") && value !== void 0) result[key] = parseScalar(value);
148
+ }
149
+ return result;
150
+ }
151
+ /** Requires non-empty Agent Skills frontmatter fields. */
152
+ function requireSkillFrontmatter(frontmatter, subject) {
153
+ if (frontmatter.name === null || frontmatter.name.trim() === "") throw new Error(`${subject} requires a name (\`name:\`) field in SKILL.md frontmatter.`);
154
+ if (frontmatter.description === null || frontmatter.description.trim() === "") throw new Error(`${subject} requires a description (\`description:\`) field in SKILL.md frontmatter.`);
155
+ return {
156
+ name: frontmatter.name,
157
+ description: frontmatter.description
158
+ };
159
+ }
160
+ /**
161
+ * Asserts that `realPath` lies inside (or equals) `canonicalRoot`.
162
+ *
163
+ * The check is performed on canonicalized paths (after `realpath`) so
164
+ * symlinks pointing outside the skill directory root are reliably rejected.
165
+ */
166
+ function assertInsideRoot(realPath, canonicalRoot, originalPath) {
167
+ if (!isWithin(canonicalRoot, realPath)) throw new Error(`Extra skill path traversal rejected: "${originalPath}" resolves to "${realPath}", which is outside the skill directory root "${canonicalRoot}".`);
168
+ }
169
+ /**
170
+ * Recursively collects file paths under `dir`, rejecting any entry that
171
+ * escapes `canonicalRoot` via symlink.
172
+ *
173
+ * Bundle contents are copied as authored — no implicit name-based filtering.
174
+ * Bundle authors are responsible for keeping `sourceDir` clean (no
175
+ * `node_modules/`, `.git/`, editor cruft, etc.).
176
+ *
177
+ * Cycle protection: directories are tracked by their canonical realpath in
178
+ * `visitedDirs` so symlinks like `loop -> .` or `a/back -> ..` (which all
179
+ * pass the inside-root guard) cannot drive unbounded recursion. The first
180
+ * occurrence is walked; further occurrences are silently skipped.
181
+ */
182
+ async function collectBundleEntries(dir, canonicalRoot, relPrefix, visitedDirs) {
183
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
184
+ const collected = [];
185
+ for (const entry of entries) {
186
+ const name = entry.name;
187
+ const absPath = join(dir, name);
188
+ const relPath = relPrefix === "" ? name : `${relPrefix}/${name}`;
189
+ const realPath = await realpath(absPath);
190
+ assertInsideRoot(realPath, canonicalRoot, absPath);
191
+ const realStat = await stat(realPath);
192
+ if (realStat.isDirectory()) {
193
+ if (visitedDirs.has(realPath)) continue;
194
+ visitedDirs.add(realPath);
195
+ collected.push(...await collectBundleEntries(absPath, canonicalRoot, relPath, visitedDirs));
196
+ } else if (realStat.isFile()) collected.push({
197
+ relPath,
198
+ absPath
199
+ });
200
+ }
201
+ return collected;
202
+ }
203
+ /**
204
+ * Loads the contents of a hand-authored skill bundle and extracts the
205
+ * `name`/`description` it declares in its `SKILL.md` frontmatter.
206
+ *
207
+ * Steps:
208
+ * 1. Resolve `sourceDir` (URL / absolute / relative-from-package-root).
209
+ * 2. `realpath` the resolved path; reject if not a directory.
210
+ * 3. Recursively walk, applying a per-entry path-traversal guard against the
211
+ * canonical root and a directory-cycle guard.
212
+ * 4. Verify `SKILL.md` exists at the bundle root.
213
+ * 5. Probe the frontmatter for `name:` and `description:` — both are required.
214
+ *
215
+ * The returned `frontmatter` becomes the source of truth for the build
216
+ * pipeline and output paths. Crust does not rewrite `SKILL.md`; the bundle
217
+ * author owns it.
218
+ *
219
+ * @internal Exported for unit testing.
220
+ */
221
+ async function loadBundleFiles(sourceDir) {
222
+ const resolved = resolveSourceDir(sourceDir);
223
+ let canonicalRoot;
224
+ try {
225
+ canonicalRoot = await realpath(resolved);
226
+ } catch (err) {
227
+ throw new Error(`Extra skill directory "${resolved}" does not exist or is not accessible.`, { cause: err });
228
+ }
229
+ if (!(await stat(canonicalRoot)).isDirectory()) throw new Error(`Extra skill path "${canonicalRoot}" is not a directory.`);
230
+ const collected = await collectBundleEntries(canonicalRoot, canonicalRoot, "", /* @__PURE__ */ new Set([canonicalRoot]));
231
+ const skillMd = collected.find((f) => f.relPath === SKILL_MD);
232
+ if (!skillMd) throw new Error(`Extra skill directory is missing SKILL.md at its root "${canonicalRoot}". Every extra skill directory must contain a top-level SKILL.md file.`);
233
+ return {
234
+ files: await Promise.all(collected.map(async (entry) => ({
235
+ path: entry.relPath,
236
+ content: await readFile(entry.absPath)
237
+ }))),
238
+ frontmatter: requireSkillFrontmatter(probeFrontmatter(await readFile(skillMd.absPath, "utf-8")), `Extra skill SKILL.md at "${join(canonicalRoot, SKILL_MD)}"`)
239
+ };
240
+ }
241
+ //#endregion
242
+ //#region src/errors.ts
243
+ var SkillSourceConflictError = class extends Error {
244
+ name = "SkillSourceConflictError";
245
+ skillName;
246
+ constructor(skillName) {
247
+ super(`Skill source name conflict: "${skillName}" is declared more than once.`);
248
+ this.skillName = skillName;
249
+ }
250
+ };
251
+ /** Refuses to overwrite an agent entry that is not owned by the requested skill. */
252
+ var SkillConflictError = class extends Error {
253
+ name = "SkillConflictError";
254
+ details;
255
+ constructor(details) {
256
+ super(`Skill conflict for agent "${details.agent}": entry "${details.outputDir}" is not owned by the requested skill.`);
257
+ this.details = details;
258
+ }
259
+ };
260
+ //#endregion
261
+ //#region ../utils/src/process.ts
262
+ /** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
263
+ function which(command) {
264
+ if (command.includes(sep) || command.includes("/")) {
265
+ try {
266
+ accessSync(command, constants.X_OK);
267
+ if (statSync(command).isFile()) return command;
268
+ } catch {}
269
+ return null;
270
+ }
271
+ const extensions = process.platform === "win32" && !extname(command) ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
272
+ for (const directory of process.env.PATH?.split(delimiter) ?? []) for (const extension of extensions) {
273
+ const candidate = resolve(join(directory, command + extension));
274
+ try {
275
+ accessSync(candidate, constants.X_OK);
276
+ if (statSync(candidate).isFile()) return candidate;
277
+ } catch {}
278
+ }
279
+ return null;
280
+ }
281
+ //#endregion
282
+ //#region src/agents.ts
283
+ const PROJECT_UNIVERSAL_SKILLS_DIR = join(".agents", "skills");
284
+ function configHome(home) {
285
+ if (home !== homedir()) return join(home, ".config");
286
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
287
+ return xdg && xdg.length > 0 ? xdg : join(home, ".config");
288
+ }
289
+ function universalGlobalSkillsDir(home) {
290
+ return join(home, ".agents", "skills");
291
+ }
292
+ const universal = (label) => ({
293
+ label,
294
+ class: "universal",
295
+ projectSkillsDir: PROJECT_UNIVERSAL_SKILLS_DIR,
296
+ globalSkillsDir: universalGlobalSkillsDir
297
+ });
298
+ /** @internal */
299
+ function resolveEffectiveScope(scope) {
300
+ return scope === "project" && process.cwd() === homedir() ? "global" : scope;
301
+ }
302
+ const AGENTS = {
303
+ amp: universal("Amp"),
304
+ adal: {
305
+ label: "AdaL",
306
+ class: "additional",
307
+ projectSkillsDir: join(".adal", "skills"),
308
+ globalSkillsDir: (home) => join(home, ".adal", "skills"),
309
+ detectCommands: ["adal"]
310
+ },
311
+ antigravity: {
312
+ label: "Antigravity",
313
+ class: "additional",
314
+ projectSkillsDir: PROJECT_UNIVERSAL_SKILLS_DIR,
315
+ globalSkillsDir: (home) => join(home, ".gemini", "config", "skills"),
316
+ detectCommands: ["antigravity"]
317
+ },
318
+ augment: {
319
+ label: "Augment",
320
+ class: "additional",
321
+ projectSkillsDir: join(".augment", "skills"),
322
+ globalSkillsDir: (home) => join(home, ".augment", "skills"),
323
+ detectCommands: ["augment"]
324
+ },
325
+ "claude-code": {
326
+ label: "Claude Code",
327
+ class: "additional",
328
+ projectSkillsDir: join(".claude", "skills"),
329
+ globalSkillsDir: (home) => join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"), "skills"),
330
+ detectCommands: ["claude", "claude-code"]
331
+ },
332
+ cline: universal("Cline"),
333
+ codebuddy: {
334
+ label: "CodeBuddy",
335
+ class: "additional",
336
+ projectSkillsDir: join(".codebuddy", "skills"),
337
+ globalSkillsDir: (home) => join(home, ".codebuddy", "skills"),
338
+ detectCommands: ["codebuddy"]
339
+ },
340
+ codex: universal("Codex"),
341
+ "command-code": {
342
+ label: "Command Code",
343
+ class: "additional",
344
+ projectSkillsDir: join(".commandcode", "skills"),
345
+ globalSkillsDir: (home) => join(home, ".commandcode", "skills"),
346
+ detectCommands: ["command-code", "commandcode"]
347
+ },
348
+ continue: {
349
+ label: "Continue",
350
+ class: "additional",
351
+ projectSkillsDir: join(".continue", "skills"),
352
+ globalSkillsDir: (home) => join(home, ".continue", "skills"),
353
+ detectCommands: ["continue"]
354
+ },
355
+ cortex: {
356
+ label: "Cortex Code",
357
+ class: "additional",
358
+ projectSkillsDir: join(".cortex", "skills"),
359
+ globalSkillsDir: (home) => join(home, ".snowflake", "cortex", "skills"),
360
+ detectCommands: ["cortex"]
361
+ },
362
+ crush: {
363
+ label: "Crush",
364
+ class: "additional",
365
+ projectSkillsDir: join(".crush", "skills"),
366
+ globalSkillsDir: (home) => join(configHome(home), "crush", "skills"),
367
+ detectCommands: ["crush"]
368
+ },
369
+ cursor: universal("Cursor"),
370
+ droid: {
371
+ label: "Droid",
372
+ class: "additional",
373
+ projectSkillsDir: join(".factory", "skills"),
374
+ globalSkillsDir: (home) => join(home, ".factory", "skills"),
375
+ detectCommands: ["droid"]
376
+ },
377
+ "gemini-cli": universal("Gemini CLI"),
378
+ "github-copilot": universal("GitHub Copilot"),
379
+ goose: {
380
+ label: "Goose",
381
+ class: "additional",
382
+ projectSkillsDir: join(".goose", "skills"),
383
+ globalSkillsDir: (home) => join(configHome(home), "goose", "skills"),
384
+ detectCommands: ["goose"]
385
+ },
386
+ "iflow-cli": {
387
+ label: "iFlow CLI",
388
+ class: "additional",
389
+ projectSkillsDir: join(".iflow", "skills"),
390
+ globalSkillsDir: (home) => join(home, ".iflow", "skills"),
391
+ detectCommands: ["iflow", "iflow-cli"]
392
+ },
393
+ junie: {
394
+ label: "Junie",
395
+ class: "additional",
396
+ projectSkillsDir: join(".junie", "skills"),
397
+ globalSkillsDir: (home) => join(home, ".junie", "skills"),
398
+ detectCommands: ["junie"]
399
+ },
400
+ kilo: {
401
+ label: "Kilo Code",
402
+ class: "additional",
403
+ projectSkillsDir: join(".kilocode", "skills"),
404
+ globalSkillsDir: (home) => join(home, ".kilocode", "skills"),
405
+ detectCommands: ["kilo", "kilocode"]
406
+ },
407
+ "kimi-cli": universal("Kimi Code CLI"),
408
+ "kiro-cli": {
409
+ label: "Kiro CLI",
410
+ class: "additional",
411
+ projectSkillsDir: join(".kiro", "skills"),
412
+ globalSkillsDir: (home) => join(home, ".kiro", "skills"),
413
+ detectCommands: ["kiro", "kiro-cli"]
414
+ },
415
+ kode: {
416
+ label: "Kode",
417
+ class: "additional",
418
+ projectSkillsDir: join(".kode", "skills"),
419
+ globalSkillsDir: (home) => join(home, ".kode", "skills"),
420
+ detectCommands: ["kode"]
421
+ },
422
+ mcpjam: {
423
+ label: "MCPJam",
424
+ class: "additional",
425
+ projectSkillsDir: join(".mcpjam", "skills"),
426
+ globalSkillsDir: (home) => join(home, ".mcpjam", "skills"),
427
+ detectCommands: ["mcpjam"]
428
+ },
429
+ "mistral-vibe": {
430
+ label: "Mistral Vibe",
431
+ class: "additional",
432
+ projectSkillsDir: join(".vibe", "skills"),
433
+ globalSkillsDir: (home) => join(process.env.VIBE_HOME || join(home, ".vibe"), "skills"),
434
+ detectCommands: ["mistral-vibe", "vibe"]
435
+ },
436
+ mux: {
437
+ label: "Mux",
438
+ class: "additional",
439
+ projectSkillsDir: join(".mux", "skills"),
440
+ globalSkillsDir: (home) => join(home, ".mux", "skills"),
441
+ detectCommands: ["mux"]
442
+ },
443
+ neovate: {
444
+ label: "Neovate",
445
+ class: "additional",
446
+ projectSkillsDir: join(".neovate", "skills"),
447
+ globalSkillsDir: (home) => join(home, ".neovate", "skills"),
448
+ detectCommands: ["neovate"]
449
+ },
450
+ opencode: universal("OpenCode"),
451
+ openclaw: {
452
+ label: "OpenClaw",
453
+ class: "additional",
454
+ projectSkillsDir: "skills",
455
+ globalSkillsDir: (home) => join(home, ".openclaw", "skills"),
456
+ detectCommands: ["openclaw"]
457
+ },
458
+ openhands: {
459
+ label: "OpenHands",
460
+ class: "additional",
461
+ projectSkillsDir: join(".openhands", "skills"),
462
+ globalSkillsDir: (home) => join(home, ".openhands", "skills"),
463
+ detectCommands: ["openhands"]
464
+ },
465
+ pi: universal("Pi"),
466
+ pochi: {
467
+ label: "Pochi",
468
+ class: "additional",
469
+ projectSkillsDir: join(".pochi", "skills"),
470
+ globalSkillsDir: (home) => join(home, ".pochi", "skills"),
471
+ detectCommands: ["pochi"]
472
+ },
473
+ qoder: {
474
+ label: "Qoder",
475
+ class: "additional",
476
+ projectSkillsDir: join(".qoder", "skills"),
477
+ globalSkillsDir: (home) => join(home, ".qoder", "skills"),
478
+ detectCommands: ["qoder"]
479
+ },
480
+ "qwen-code": {
481
+ label: "Qwen Code",
482
+ class: "additional",
483
+ projectSkillsDir: join(".qwen", "skills"),
484
+ globalSkillsDir: (home) => join(home, ".qwen", "skills"),
485
+ detectCommands: ["qwen", "qwen-code"]
486
+ },
487
+ replit: universal("Replit"),
488
+ roo: {
489
+ label: "Roo Code",
490
+ class: "additional",
491
+ projectSkillsDir: join(".roo", "skills"),
492
+ globalSkillsDir: (home) => join(home, ".roo", "skills"),
493
+ detectCommands: ["roo", "roo-code"]
494
+ },
495
+ trae: {
496
+ label: "Trae",
497
+ class: "additional",
498
+ projectSkillsDir: join(".trae", "skills"),
499
+ globalSkillsDir: (home) => join(home, ".trae", "skills"),
500
+ detectCommands: ["trae"]
501
+ },
502
+ "trae-cn": {
503
+ label: "Trae CN",
504
+ class: "additional",
505
+ projectSkillsDir: join(".trae", "skills"),
506
+ globalSkillsDir: (home) => join(home, ".trae-cn", "skills"),
507
+ detectCommands: ["trae-cn", "trae"]
508
+ },
509
+ warp: universal("Warp"),
510
+ windsurf: {
511
+ label: "Windsurf",
512
+ class: "additional",
513
+ projectSkillsDir: join(".windsurf", "skills"),
514
+ globalSkillsDir: (home) => join(home, ".codeium", "windsurf", "skills"),
515
+ detectCommands: ["windsurf"]
516
+ },
517
+ zed: universal("Zed"),
518
+ zencoder: {
519
+ label: "Zencoder",
520
+ class: "additional",
521
+ projectSkillsDir: join(".zencoder", "skills"),
522
+ globalSkillsDir: (home) => join(home, ".zencoder", "skills"),
523
+ detectCommands: ["zencoder"]
524
+ }
525
+ };
526
+ /**
527
+ * All agent targets supported by `@crustjs/skills`.
528
+ *
529
+ * @internal
530
+ */
531
+ const ALL_AGENTS = Object.keys(AGENTS);
532
+ /**
533
+ * Human-readable labels for each agent target.
534
+ *
535
+ * @internal
536
+ */
537
+ const AGENT_LABELS = Object.fromEntries(ALL_AGENTS.map((agent) => [agent, AGENTS[agent].label]));
538
+ /** Returns agents that use the canonical `.agents/skills` layout. */
539
+ function getUniversalAgents() {
540
+ return ALL_AGENTS.filter((agent) => AGENTS[agent].class === "universal");
541
+ }
542
+ /** Returns agents that do not use the canonical layout at both scopes. */
543
+ function getAdditionalAgents() {
544
+ return ALL_AGENTS.filter((agent) => AGENTS[agent].class === "additional");
545
+ }
546
+ /** Returns true if the agent uses the canonical layout at both scopes. */
547
+ function isUniversalAgent(agent) {
548
+ return AGENTS[agent].class === "universal";
549
+ }
550
+ /**
551
+ * Detects installed non-universal agents by checking PATH for their CLI binaries.
552
+ *
553
+ * Universal agents are intentionally not detected here so callers can always
554
+ * present them as a single optional "Universal" install target.
555
+ */
556
+ async function detectInstalledAgents() {
557
+ return getAdditionalAgents().filter((agent) => (AGENTS[agent].detectCommands ?? []).some((command) => which(command) !== null));
558
+ }
559
+ /**
560
+ * Resolves the filesystem path for a skill installation.
561
+ *
562
+ * @internal
563
+ */
564
+ function resolveAgentPath(agent, scope, name) {
565
+ const cfg = AGENTS[agent];
566
+ if (scope === "project") return join(process.cwd(), cfg.projectSkillsDir, name);
567
+ return join(cfg.globalSkillsDir(homedir()), name);
568
+ }
569
+ //#endregion
570
+ //#region src/link.ts
571
+ /** Returns whether a symlink target carries Crust's skill ownership signature. */
572
+ function isOwnedSkillLink(target, name) {
573
+ const segments = target.replaceAll("\\", "/").split("/").filter(Boolean);
574
+ return segments.at(-2) === "skills" && segments.at(-1) === name;
575
+ }
576
+ function skillLinkTarget(sourceDir, outputDir, scope) {
577
+ return scope === "project" ? relative(dirname(outputDir), sourceDir) : sourceDir;
578
+ }
579
+ //#endregion
580
+ //#region src/skill-name.ts
581
+ /**
582
+ * Agent Skills spec name pattern: 1–64 lowercase alphanumeric characters and
583
+ * hyphens. Must not start or end with `-`, and must not contain consecutive `--`.
584
+ */
585
+ const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
586
+ /**
587
+ * Validates a resolved skill name against the Agent Skills specification.
588
+ *
589
+ * @param name - The resolved skill name to validate
590
+ * @returns `true` if valid, `false` otherwise
591
+ */
592
+ function isValidSkillName(name) {
593
+ return name.length >= 1 && name.length <= 64 && SKILL_NAME_PATTERN.test(name);
594
+ }
595
+ //#endregion
596
+ //#region ../utils/src/error.ts
597
+ /** Narrows a caught Node.js system error to its stable errno contract. */
598
+ function isErrnoException(error) {
599
+ return error instanceof Error && "code" in error && typeof error.code === "string";
600
+ }
601
+ //#endregion
602
+ //#region src/source.ts
603
+ var SkillSourceUnavailableError = class extends Error {
604
+ name = "SkillSourceUnavailableError";
605
+ };
606
+ function directoryPath(path) {
607
+ try {
608
+ return statSync(path).isDirectory() ? path : null;
609
+ } catch {
610
+ return null;
611
+ }
612
+ }
613
+ function fallbackName(source) {
614
+ if (source instanceof URL) return basename(fileURLToPath(source));
615
+ return isAbsolute(source) ? basename(source) : source;
616
+ }
617
+ /**
618
+ * Resolves a logical packaged skill-source root. When the package path is
619
+ * unavailable, falls back to the executable directory: absolute and URL
620
+ * sources by their basename, relative sources by the same relative path.
621
+ */
622
+ function resolveSkillSource(source) {
623
+ if (source instanceof URL && source.protocol !== "file:") throw new Error(`Skill source URL must use file: protocol, got "${source.protocol}".`);
624
+ let primary;
625
+ try {
626
+ primary = resolveSourceDir(source);
627
+ } catch {}
628
+ if (primary) {
629
+ const resolved = directoryPath(primary);
630
+ if (resolved) return resolved;
631
+ }
632
+ const fallback = join(dirname(process.execPath), fallbackName(source));
633
+ const resolvedFallback = directoryPath(fallback);
634
+ if (resolvedFallback) return resolvedFallback;
635
+ throw new SkillSourceUnavailableError(`Could not resolve skill source${primary ? ` at "${primary}"` : ""} or executable-relative fallback "${fallback}".`);
636
+ }
637
+ function readSkillFrontmatter(sourceDir) {
638
+ let content;
639
+ try {
640
+ content = readFileSync(join(sourceDir, "SKILL.md"), "utf8");
641
+ } catch (error) {
642
+ if (!isErrnoException(error) || error.code !== "ENOENT") throw error;
643
+ throw new Error(`Skill source directory "${sourceDir}" is missing SKILL.md.`, { cause: error });
644
+ }
645
+ return requireSkillFrontmatter(probeFrontmatter(content), `Skill source directory "${sourceDir}"`);
646
+ }
647
+ /** Reads every self-describing skill directory in a packaged skill source. */
648
+ function loadPackagedSkills(source) {
649
+ const root = resolveSkillSource(source);
650
+ const skills = [];
651
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
652
+ if (!entry.isDirectory()) continue;
653
+ const sourceDir = join(root, entry.name);
654
+ if (!existsSync(join(sourceDir, "SKILL.md"))) continue;
655
+ const frontmatter = readSkillFrontmatter(sourceDir);
656
+ if (frontmatter.name !== entry.name) throw new Error(`Skill source directory "${sourceDir}" declares name "${frontmatter.name}" in SKILL.md.`);
657
+ skills.push({
658
+ sourceDir,
659
+ name: frontmatter.name,
660
+ description: frontmatter.description
661
+ });
662
+ }
663
+ if (skills.length === 0) throw new SkillSourceUnavailableError(`Skill source "${root}" does not contain any skill directories.`);
664
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
665
+ }
666
+ //#endregion
667
+ //#region src/generate.ts
668
+ /** @internal */
669
+ function groupAgentsByOutputDir(agents, scope, name) {
670
+ const groups = /* @__PURE__ */ new Map();
671
+ for (const agent of agents) {
672
+ const outputDir = resolveAgentPath(agent, scope, name);
673
+ const existing = groups.get(outputDir);
674
+ if (existing) existing.push(agent);
675
+ else groups.set(outputDir, [agent]);
676
+ }
677
+ return groups;
678
+ }
679
+ async function pathExists(path) {
680
+ try {
681
+ await stat(path);
682
+ return true;
683
+ } catch {
684
+ return false;
685
+ }
686
+ }
687
+ async function inspectLink(outputDir, name, expectedSourceDir) {
688
+ let entry;
689
+ try {
690
+ entry = await lstat(outputDir);
691
+ } catch {
692
+ return { status: "absent" };
693
+ }
694
+ if (!entry.isSymbolicLink()) return { status: "conflict" };
695
+ const target = await readlink(outputDir);
696
+ if (!isOwnedSkillLink(target, name)) return { status: "conflict" };
697
+ const resolvedTarget = resolve(dirname(outputDir), target);
698
+ return {
699
+ status: "owned",
700
+ resolves: await pathExists(resolvedTarget),
701
+ correct: expectedSourceDir === void 0 || resolvedTarget === resolve(expectedSourceDir)
702
+ };
703
+ }
704
+ async function createSkillLink(target, outputDir) {
705
+ await mkdir(dirname(outputDir), { recursive: true });
706
+ try {
707
+ await symlink(target, outputDir, "dir");
708
+ } catch (error) {
709
+ const detail = error instanceof Error ? ` ${error.message}` : "";
710
+ throw new Error(`Could not create skill symlink "${outputDir}" -> "${target}".${detail} If permission was denied, enable symlink permission for this environment and try again.`, { cause: error });
711
+ }
712
+ }
713
+ /** Links one packaged skill source into the requested agent directories. */
714
+ async function installSkill(options) {
715
+ const sourceDir = resolveSourceDir(options.sourceDir);
716
+ const source = readSkillFrontmatter(sourceDir);
717
+ if (!isValidSkillName(source.name)) throw new Error(`Skill source "${sourceDir}" declares invalid name "${source.name}".`);
718
+ if (!isOwnedSkillLink(sourceDir, source.name)) throw new Error(`Skill source directory "${sourceDir}" must be named "skills/${source.name}" to support ownership-safe links.`);
719
+ const agents = options.agents ?? [...getUniversalAgents(), ...await detectInstalledAgents()];
720
+ const scope = resolveEffectiveScope(options.scope ?? "global");
721
+ const results = [];
722
+ for (const [outputDir, groupedAgents] of groupAgentsByOutputDir(agents, scope, source.name)) {
723
+ const inspection = await inspectLink(outputDir, source.name, sourceDir);
724
+ if (inspection.status === "conflict" && options.force !== true) throw new SkillConflictError({
725
+ agent: groupedAgents[0],
726
+ outputDir
727
+ });
728
+ const upToDate = inspection.status === "owned" && inspection.resolves && inspection.correct && options.force !== true;
729
+ const status = upToDate ? "up-to-date" : inspection.status === "absent" ? "installed" : "repaired";
730
+ if (!upToDate) {
731
+ if (inspection.status !== "absent") await rm(outputDir, {
732
+ recursive: true,
733
+ force: true
734
+ });
735
+ await createSkillLink(skillLinkTarget(sourceDir, outputDir, scope), outputDir);
736
+ }
737
+ for (const agent of groupedAgents) results.push({
738
+ agent,
739
+ outputDir,
740
+ scope,
741
+ status
742
+ });
743
+ }
744
+ return { agents: results };
745
+ }
746
+ /** Unlinks only agent-directory entries carrying the requested skill's ownership signature. */
747
+ async function uninstallSkill(options) {
748
+ const agents = options.agents ?? [...ALL_AGENTS];
749
+ const scope = resolveEffectiveScope(options.scope ?? "global");
750
+ const results = [];
751
+ for (const [outputDir, groupedAgents] of groupAgentsByOutputDir(agents, scope, options.name)) {
752
+ const removed = (await inspectLink(outputDir, options.name)).status === "owned";
753
+ if (removed) await unlink(outputDir);
754
+ for (const agent of groupedAgents) results.push({
755
+ agent,
756
+ outputDir,
757
+ scope,
758
+ status: removed ? "removed" : "not-found"
759
+ });
760
+ }
761
+ return { agents: results };
762
+ }
763
+ /** Reports the ownership and health of each requested agent-directory entry. */
764
+ async function getSkillStatus(options) {
765
+ const agents = options.agents ?? [...ALL_AGENTS];
766
+ const scope = resolveEffectiveScope(options.scope ?? "global");
767
+ const expectedSourceDir = resolveSourceDir(options.sourceDir);
768
+ const results = [];
769
+ for (const [outputDir, groupedAgents] of groupAgentsByOutputDir(agents, scope, options.name)) {
770
+ const inspection = await inspectLink(outputDir, options.name, expectedSourceDir);
771
+ const status = inspection.status === "owned" ? inspection.resolves && inspection.correct ? "linked" : "dangling" : inspection.status;
772
+ for (const agent of groupedAgents) results.push({
773
+ agent,
774
+ outputDir,
775
+ scope,
776
+ status
777
+ });
778
+ }
779
+ return { agents: results };
780
+ }
781
+ //#endregion
782
+ //#region src/reconcile.ts
783
+ const UNIVERSAL_GROUP = "__universal__";
784
+ /** @internal Plans link changes without performing prompt or filesystem I/O. */
785
+ function planReconcile(options) {
786
+ const { statusMap, choices, selected, universal } = options;
787
+ const installed = [...statusMap.values()].flatMap((entry) => entry.status === "linked" || entry.status === "dangling" ? [entry.agent] : []);
788
+ const toInstall = selected.filter((agent) => statusMap.get(agent)?.status !== "linked");
789
+ const keptDirs = new Set(selected.map((agent) => statusMap.get(agent)?.outputDir));
790
+ return {
791
+ toInstall,
792
+ toUninstall: installed.filter((agent) => !selected.includes(agent) && !keptDirs.has(statusMap.get(agent)?.outputDir)),
793
+ sharedDirWarnings: choices.flatMap((choice) => {
794
+ const agent = choice.value === "__universal__" ? universal[0] : choice.value;
795
+ const entry = statusMap.get(agent);
796
+ return !selected.includes(agent) && entry && keptDirs.has(entry.outputDir) ? [{
797
+ label: choice.label,
798
+ outputDir: entry.outputDir
799
+ }] : [];
800
+ })
801
+ };
802
+ }
803
+ //#endregion
804
+ //#region src/extension.ts
805
+ const SKILLS = defineExtensionId("crust:skills");
806
+ const DEFAULT_SKILL_COMMAND_NAME = "skill";
807
+ const SKILLS_SECTION_TITLE = "Agent skills";
808
+ const DEFAULT_SKILL_SCOPE = "global";
809
+ async function resolveScope(rawScope, options) {
810
+ if (rawScope) return rawScope;
811
+ if (options.defaultScope) return options.defaultScope;
812
+ return select({
813
+ message: "Select scope",
814
+ choices: [{
815
+ label: "Project",
816
+ value: "project"
817
+ }, {
818
+ label: "Global",
819
+ value: "global"
820
+ }],
821
+ default: DEFAULT_SKILL_SCOPE
822
+ });
823
+ }
824
+ function formatAgentLabels(agents) {
825
+ const universal = new Set(getUniversalAgents());
826
+ const labels = agents.some((agent) => universal.has(agent)) ? ["Universal"] : [];
827
+ for (const agent of agents) if (!universal.has(agent)) labels.push(AGENT_LABELS[agent]);
828
+ return labels;
829
+ }
830
+ async function repairInstalledSkill(packagedSkill, scope, io, report = false) {
831
+ const status = await getSkillStatus({
832
+ name: packagedSkill.name,
833
+ sourceDir: packagedSkill.sourceDir,
834
+ scope
835
+ });
836
+ const effectiveScope = status.agents[0]?.scope ?? scope;
837
+ for (const outputDir of new Set(status.agents.filter((entry) => entry.status === "conflict").map((entry) => entry.outputDir))) io.stderr(yellow(`Skill conflict [${packagedSkill.name}]: "${outputDir}" is not owned by this skill. Skipping link repair.`));
838
+ const stale = status.agents.filter((entry) => entry.status === "dangling");
839
+ if (stale.length === 0) {
840
+ if (report) io.stdout(dim(`No repairs needed [${packagedSkill.name}] (${effectiveScope}).`));
841
+ return;
842
+ }
843
+ try {
844
+ const labels = formatAgentLabels((await installSkill({
845
+ sourceDir: packagedSkill.sourceDir,
846
+ agents: stale.map((entry) => entry.agent),
847
+ scope
848
+ })).agents.map((entry) => entry.agent));
849
+ if (report && labels.length > 0) io.stdout(`\n${bold(`Repaired "${packagedSkill.name}" for ${labels.join(", ")} (${effectiveScope})`)}`);
850
+ } catch (error) {
851
+ if (!(error instanceof SkillConflictError)) throw error;
852
+ io.stderr(yellow(`Skill conflict [${packagedSkill.name}]: "${error.details.outputDir}" is not owned by this skill. Skipping link repair.`));
853
+ }
854
+ }
855
+ async function autoRepairSkills(options, io) {
856
+ let skills;
857
+ try {
858
+ skills = loadPackagedSkills(options.distDir);
859
+ } catch (error) {
860
+ if (!(error instanceof SkillSourceUnavailableError)) io.stderr(yellow(`Skipping skill link repair: ${error instanceof Error ? error.message : String(error)}`));
861
+ return;
862
+ }
863
+ const configuredScopes = options.defaultScope ? [options.defaultScope] : ["project", "global"];
864
+ const scopes = [...new Set(configuredScopes.map(resolveEffectiveScope))];
865
+ for (const packagedSkill of skills) for (const scope of scopes) try {
866
+ await repairInstalledSkill(packagedSkill, scope, io);
867
+ } catch (error) {
868
+ io.stderr(yellow(`Skipping skill link repair [${packagedSkill.name}]: ${error instanceof Error ? error.message : String(error)}`));
869
+ }
870
+ }
871
+ function formatSkillDocumentation(source, commandName, appName) {
872
+ try {
873
+ return loadPackagedSkills(source).map((packagedSkill) => {
874
+ const sourcePath = isWithin(process.cwd(), packagedSkill.sourceDir) ? relative(process.cwd(), packagedSkill.sourceDir) || "." : packagedSkill.sourceDir;
875
+ return `${packagedSkill.name} — ${packagedSkill.description}\n Source: ${sourcePath}`;
876
+ }).join("\n\n");
877
+ } catch (error) {
878
+ if (error instanceof SkillSourceUnavailableError) return `The packaged skills directory is unavailable. Run \`${appName} ${commandName}\` to link packaged skills into an agent directory.`;
879
+ return `Packaged skills could not be read. Run \`${appName} ${commandName}\` for details.`;
880
+ }
881
+ }
882
+ async function buildSkills(options, context) {
883
+ const { writeSkills, writeSkillsFromSnapshot } = await Promise.resolve().then(() => build_exports);
884
+ const writeOptions = {
885
+ outDir: join(context.outDir, "skills"),
886
+ version: context.snapshot.meta.version,
887
+ name: options.name,
888
+ description: options.description,
889
+ extras: options.extras
890
+ };
891
+ return (options.generated === false ? await writeSkills(writeOptions) : await writeSkillsFromSnapshot(context.snapshot, writeOptions)).map((file) => join("skills", file));
892
+ }
893
+ const skill = defineExtension(SKILLS, (options) => {
894
+ const commandName = options.command ?? DEFAULT_SKILL_COMMAND_NAME;
895
+ return {
896
+ commands: [buildSkillCommand(commandName, options)],
897
+ sections: (snapshot) => [{
898
+ command: [],
899
+ title: SKILLS_SECTION_TITLE,
900
+ body: formatSkillDocumentation(options.distDir, commandName, snapshot.meta.name),
901
+ except: [SKILLS]
902
+ }],
903
+ build: (context) => buildSkills(options, context),
904
+ hooks: { async preRun(context) {
905
+ if (context.commandPath[1] === commandName || options.autoUpdate === false) return;
906
+ await autoRepairSkills(options, context);
907
+ } }
908
+ };
909
+ });
910
+ async function reconcileSkill(opts) {
911
+ const { packagedSkill, scope, installAll, io } = opts;
912
+ const effectiveScope = resolveEffectiveScope(scope);
913
+ const detected = new Set(await detectInstalledAgents());
914
+ const universal = getUniversalAgents();
915
+ const status = await getSkillStatus({
916
+ name: packagedSkill.name,
917
+ sourceDir: packagedSkill.sourceDir,
918
+ scope: effectiveScope
919
+ });
920
+ const statusMap = new Map(status.agents.map((entry) => [entry.agent, entry]));
921
+ const installed = new Set(status.agents.filter((entry) => entry.status === "linked" || entry.status === "dangling").map((entry) => entry.agent));
922
+ const additional = getAdditionalAgents().filter((agent) => detected.has(agent) || installed.has(agent));
923
+ const choices = [];
924
+ if (universal.length > 0) choices.push({
925
+ label: "Universal",
926
+ value: UNIVERSAL_GROUP,
927
+ hint: statusMap.get(universal[0])?.outputDir ?? "path unavailable"
928
+ });
929
+ for (const agent of additional) choices.push({
930
+ label: AGENT_LABELS[agent],
931
+ value: agent,
932
+ hint: statusMap.get(agent)?.outputDir ?? "path unavailable"
933
+ });
934
+ let selected;
935
+ if (installAll) selected = [...universal, ...additional];
936
+ else {
937
+ const defaults = additional.filter((agent) => installed.has(agent));
938
+ if (universal.length > 0 && universal.every((agent) => installed.has(agent))) defaults.unshift(UNIVERSAL_GROUP);
939
+ const values = await multiselect({
940
+ message: `Select agents to install "${packagedSkill.name}" for`,
941
+ choices,
942
+ default: defaults,
943
+ required: false
944
+ });
945
+ selected = values.filter((value) => value !== UNIVERSAL_GROUP);
946
+ if (values.includes("__universal__")) selected.push(...universal);
947
+ }
948
+ const { toInstall, toUninstall, sharedDirWarnings } = planReconcile({
949
+ statusMap,
950
+ choices,
951
+ selected,
952
+ universal
953
+ });
954
+ for (const warning of sharedDirWarnings) io.stderr(yellow(`${warning.label} [${packagedSkill.name}]: "${warning.outputDir}" is shared with a selected agent, so the skill stays available to it.`));
955
+ if (toInstall.length > 0) {
956
+ const groups = groupAgentsByOutputDir(toInstall, effectiveScope, packagedSkill.name);
957
+ const installedAgents = [];
958
+ for (const agents of groups.values()) {
959
+ const runInstall = (force) => installSkill({
960
+ sourceDir: packagedSkill.sourceDir,
961
+ agents,
962
+ scope: effectiveScope,
963
+ force
964
+ });
965
+ try {
966
+ const result = await spinner({
967
+ message: `Installing skill [${packagedSkill.name}]...`,
968
+ task: () => runInstall()
969
+ });
970
+ installedAgents.push(...result.agents);
971
+ } catch (error) {
972
+ if (!(error instanceof SkillConflictError)) throw error;
973
+ const skipped = `Skipped ${formatAgentLabels(agents).join(", ")} [${packagedSkill.name}]: directory is not owned by this skill.`;
974
+ if (installAll) {
975
+ io.stderr(yellow(skipped));
976
+ continue;
977
+ }
978
+ if (!await confirm({
979
+ message: `"${error.details.outputDir}" is not owned by "${packagedSkill.name}". Overwrite?`,
980
+ default: false
981
+ })) {
982
+ io.stdout(dim(skipped));
983
+ continue;
984
+ }
985
+ const result = await runInstall(true);
986
+ installedAgents.push(...result.agents);
987
+ }
988
+ }
989
+ if (installedAgents.length > 0) {
990
+ io.stdout(`\n${bold(`Installed "${packagedSkill.name}"`)}`);
991
+ for (const line of new Map(installedAgents.map((entry) => [formatAgentLabels([entry.agent])[0], entry.outputDir]))) io.stdout(dim(` ${line[0]} → ${line[1]}`));
992
+ }
993
+ }
994
+ if (toUninstall.length > 0) await spinner({
995
+ message: `Removing skill [${packagedSkill.name}]...`,
996
+ task: () => uninstallSkill({
997
+ name: packagedSkill.name,
998
+ agents: toUninstall,
999
+ scope: effectiveScope
1000
+ })
1001
+ });
1002
+ if (toInstall.length === 0 && toUninstall.length === 0) io.stdout(dim(`No changes [${packagedSkill.name}].`));
1003
+ }
1004
+ function buildSkillCommand(commandName, options) {
1005
+ return defineCommand(commandName, { description: "Manage agent skill installations" }, (command) => command.flags({
1006
+ name: "scope",
1007
+ type: "string",
1008
+ choices: ["project", "global"],
1009
+ description: "Install scope (project or global)"
1010
+ }, {
1011
+ name: "all",
1012
+ type: "boolean",
1013
+ description: "Install for all detected agents non-interactively"
1014
+ }).add(defineCommand("update", { description: "Repair installed skill links" }, (update) => update.flags({
1015
+ name: "scope",
1016
+ type: "string",
1017
+ choices: ["project", "global"],
1018
+ description: "Update scope (project or global)"
1019
+ }).action(async (context) => {
1020
+ const scope = await resolveScope(context.flags.scope, options);
1021
+ for (const packagedSkill of loadPackagedSkills(options.distDir)) await repairInstalledSkill(packagedSkill, scope, context, true);
1022
+ }))).action(async (context) => {
1023
+ const installAll = context.flags.all === true;
1024
+ const scope = installAll ? context.flags.scope ?? options.defaultScope ?? DEFAULT_SKILL_SCOPE : await resolveScope(context.flags.scope, options);
1025
+ for (const packagedSkill of loadPackagedSkills(options.distDir)) await reconcileSkill({
1026
+ packagedSkill,
1027
+ scope,
1028
+ installAll,
1029
+ io: context
1030
+ });
1031
+ }));
1032
+ }
1033
+ //#endregion
1034
+ //#region src/manifest.ts
1035
+ function buildManifest(command) {
1036
+ const model = buildCommandDocumentation(command);
1037
+ const rootName = normalizeName(model.name);
1038
+ if (model.children.some((child) => normalizeName(child.name) === rootName)) throw new Error(`Cannot generate skills when a direct subcommand has the root command name "${rootName}".`);
1039
+ return buildNode(model);
1040
+ }
1041
+ function buildNode(model) {
1042
+ return {
1043
+ name: normalizeName(model.name),
1044
+ path: model.path.map(normalizeName),
1045
+ description: model.description,
1046
+ usage: model.usage,
1047
+ sections: sectionsFor(model.sections, SKILLS).map(({ title, body }) => ({
1048
+ title,
1049
+ body
1050
+ })),
1051
+ runnable: model.hasAction,
1052
+ args: model.args.map(normalizeArg),
1053
+ flags: [...model.flags].sort((a, b) => a.name.localeCompare(b.name)).map(normalizeFlag),
1054
+ children: [...model.children].sort((a, b) => a.name.localeCompare(b.name)).map(buildNode)
1055
+ };
1056
+ }
1057
+ function normalizeName(raw) {
1058
+ return raw.trim().toLowerCase();
1059
+ }
1060
+ function normalizeArg(arg) {
1061
+ const result = {
1062
+ name: arg.name,
1063
+ type: manifestType(arg.type),
1064
+ required: arg.required,
1065
+ variadic: arg.variadic
1066
+ };
1067
+ if (arg.description !== void 0) result.description = arg.description;
1068
+ if (arg.default !== void 0) result.default = serializeDefault(arg.default);
1069
+ return result;
1070
+ }
1071
+ function normalizeFlag(flag) {
1072
+ const result = {
1073
+ name: flag.name,
1074
+ spellings: flag.spellings,
1075
+ type: manifestType(flag.type),
1076
+ required: flag.required,
1077
+ multiple: flag.multiple
1078
+ };
1079
+ if (flag.description !== void 0) result.description = flag.description;
1080
+ if (flag.default !== void 0) result.default = serializeDefault(flag.default);
1081
+ return result;
1082
+ }
1083
+ function manifestType(type) {
1084
+ return type === "number" || type === "boolean" ? type : "string";
1085
+ }
1086
+ function serializeDefault(value) {
1087
+ return formatDefault(value);
1088
+ }
1089
+ //#endregion
1090
+ //#region src/render.ts
1091
+ /**
1092
+ * Escapes a string for safe use as a YAML scalar value.
1093
+ *
1094
+ * Wraps the value in double quotes if it contains characters that would
1095
+ * break plain YAML scalars (colons, hash signs, brackets, quotes, etc.).
1096
+ * Internal double quotes are escaped with a backslash.
1097
+ */
1098
+ function escapeYaml(value) {
1099
+ if (/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(value)) return JSON.stringify(value);
1100
+ return value;
1101
+ }
1102
+ /**
1103
+ * Escapes pipe characters in text intended for markdown table cells.
1104
+ *
1105
+ * Unescaped `|` characters break markdown table structure. This replaces
1106
+ * them with `\|` while leaving already-escaped `\|` sequences untouched.
1107
+ */
1108
+ function escapeTableCell(value) {
1109
+ return value.replace(/(?<!\\)\|/g, "\\|");
1110
+ }
1111
+ /**
1112
+ * Renders a complete set of skill files from a manifest tree and metadata.
1113
+ *
1114
+ * Produces:
1115
+ * - `SKILL.md` — entrypoint with frontmatter and lazy-load instructions
1116
+ * - `commands/` — per-command markdown files mirroring the command hierarchy
1117
+ *
1118
+ * @param manifest - The canonical manifest tree from {@link buildManifest}
1119
+ * @param meta - Skill metadata for frontmatter and naming
1120
+ * @returns Array of rendered files ready for writing
1121
+ */
1122
+ function renderSkill(manifest, meta) {
1123
+ const files = [];
1124
+ const allNodes = collectNodes(manifest);
1125
+ files.push({
1126
+ path: "SKILL.md",
1127
+ content: renderSkillMd(manifest, meta, allNodes)
1128
+ });
1129
+ for (const node of allNodes) files.push({
1130
+ path: commandFilePath(node),
1131
+ content: renderCommand(node, manifest)
1132
+ });
1133
+ return files;
1134
+ }
1135
+ /**
1136
+ * Collects all nodes in the manifest tree via depth-first traversal.
1137
+ * Includes the root node and all descendants.
1138
+ */
1139
+ function collectNodes(root) {
1140
+ const nodes = [root];
1141
+ for (const child of root.children) nodes.push(...collectNodes(child));
1142
+ return nodes;
1143
+ }
1144
+ /**
1145
+ * Computes the file path for a command node within the `commands/` directory.
1146
+ *
1147
+ * The root command maps to `commands/<root-name>.md`.
1148
+ * Subcommands strip the root prefix:
1149
+ * `["cli", "remote", "add"]` → `commands/remote/add.md`
1150
+ *
1151
+ * Single-segment paths (root only) map to `commands/<name>.md`.
1152
+ */
1153
+ function commandFilePath(node) {
1154
+ if (node.path.length <= 1) return `commands/${node.name}.md`;
1155
+ return `commands/${node.path.slice(1).join("/")}.md`;
1156
+ }
1157
+ /**
1158
+ * Builds the full invocation string for a command.
1159
+ * Example: `my-cli remote add`
1160
+ */
1161
+ function commandInvocation(node) {
1162
+ return node.path.join(" ");
1163
+ }
1164
+ /**
1165
+ * Computes a relative path from one file to another within the skill directory.
1166
+ *
1167
+ * Both paths are relative to the skill root (e.g. "commands/remote/add.md").
1168
+ */
1169
+ function relativePath(from, to) {
1170
+ return posix.relative(posix.dirname(from), to);
1171
+ }
1172
+ /**
1173
+ * Renders the `SKILL.md` entrypoint file with YAML frontmatter and
1174
+ * lazy-load instructions directing agents to supporting files.
1175
+ */
1176
+ function renderSkillMd(manifest, meta, allNodes) {
1177
+ const lines = [];
1178
+ lines.push("---");
1179
+ lines.push(`name: ${escapeYaml(meta.name)}`);
1180
+ lines.push(`description: ${escapeYaml(meta.description)}`);
1181
+ if (meta.version !== void 0) {
1182
+ lines.push("metadata:");
1183
+ lines.push(` version: "${meta.version}"`);
1184
+ }
1185
+ lines.push("---");
1186
+ lines.push("");
1187
+ lines.push(`# ${meta.name}`);
1188
+ lines.push("");
1189
+ if (manifest.description) {
1190
+ lines.push(manifest.description);
1191
+ lines.push("");
1192
+ }
1193
+ lines.push(`You should use this skill when you need accurate help with \`${meta.name}\` commands, including command selection, syntax, arguments, flags, defaults, and subcommands.`);
1194
+ lines.push("");
1195
+ lines.push("## How to Use This Skill");
1196
+ lines.push("");
1197
+ lines.push("1. You must find the command that best matches the user's task from the Command Reference below.");
1198
+ lines.push("2. You must check the `Type` column before suggesting execution: `runnable` and `runnable, group` commands can be executed, while `group` commands are organizational only.");
1199
+ lines.push("3. You should read only the linked file or files you need from `commands/`.");
1200
+ lines.push("4. You must read a command's file before answering a command-specific question or suggesting that command.");
1201
+ lines.push("5. You must treat the command file as the source of truth for usage, arguments, flags, aliases, and defaults.");
1202
+ lines.push("6. If a flag, argument, alias, or default is not documented there, you must say it is not documented instead of guessing.");
1203
+ lines.push("");
1204
+ lines.push("## Command Reference");
1205
+ lines.push("");
1206
+ lines.push("You should use this table to locate the command file you need.");
1207
+ lines.push("");
1208
+ lines.push(...renderCommandReferenceTable(allNodes));
1209
+ lines.push("");
1210
+ if (manifest.runnable) {
1211
+ lines.push("## Usage");
1212
+ lines.push("");
1213
+ const rootFile = commandFilePath(manifest);
1214
+ lines.push(`The root command is directly executable. You should see [${manifest.name}](${rootFile}) for usage details.`);
1215
+ lines.push("");
1216
+ }
1217
+ return lines.join("\n");
1218
+ }
1219
+ /**
1220
+ * Renders a markdown table mapping all command paths to their
1221
+ * documentation file paths.
1222
+ */
1223
+ function renderCommandReferenceTable(allNodes) {
1224
+ const lines = [];
1225
+ lines.push("| Command | Type | Description | Documentation |");
1226
+ lines.push("| ------- | ---- | ----------- | ------------- |");
1227
+ for (const node of allNodes) {
1228
+ const invocation = commandInvocation(node);
1229
+ const filePath = commandFilePath(node);
1230
+ const type = commandType(node);
1231
+ const description = escapeTableCell(node.description || "-");
1232
+ lines.push(`| \`${invocation}\` | ${type} | ${description} | [${filePath}](${filePath}) |`);
1233
+ }
1234
+ return lines;
1235
+ }
1236
+ /**
1237
+ * Returns a human-readable label for the command type.
1238
+ */
1239
+ function commandType(node) {
1240
+ if (node.runnable && node.children.length > 0) return "runnable, group";
1241
+ if (node.runnable) return "runnable";
1242
+ return "group";
1243
+ }
1244
+ /** Renders a command markdown file with its runnable details and child links. */
1245
+ function renderCommand(node, root) {
1246
+ const lines = [...renderCommandHeading(node), ...renderCommandSections(node)];
1247
+ if (node.runnable) lines.push(...renderRunnableCommandSections(node));
1248
+ if (node.children.length > 0) lines.push(...renderSubcommandLinks(node, commandFilePath(node)));
1249
+ lines.push(...renderNavigation(node, root));
1250
+ return lines.join("\n");
1251
+ }
1252
+ function renderCommandHeading(node) {
1253
+ const lines = [`# \`${commandInvocation(node)}\``, ""];
1254
+ if (node.description) lines.push(node.description, "");
1255
+ return lines;
1256
+ }
1257
+ function renderCommandSections(node) {
1258
+ return (node.sections ?? []).flatMap((section) => [
1259
+ `## ${section.title}`,
1260
+ section.body,
1261
+ ""
1262
+ ]);
1263
+ }
1264
+ function renderRunnableCommandSections(node) {
1265
+ const lines = [
1266
+ "## Usage",
1267
+ "",
1268
+ "```",
1269
+ node.usage,
1270
+ "```",
1271
+ ""
1272
+ ];
1273
+ if (node.args.length > 0) lines.push("## Arguments", "", ...renderArgsTable(node.args), "");
1274
+ if (node.flags.length > 0) lines.push("## Flags", "", ...renderFlagsTable(node.flags), "");
1275
+ lines.push("## Command Documentation Authority", "", "You must treat only the arguments, flags, options, aliases, and defaults documented in this file as supported for this command.", "You must not infer or invent additional command-line options.", "");
1276
+ return lines;
1277
+ }
1278
+ function renderSubcommandLinks(node, filePath) {
1279
+ const lines = ["## Subcommands", ""];
1280
+ for (const child of node.children) {
1281
+ const childRelative = relativePath(filePath, commandFilePath(child));
1282
+ const desc = child.description ? ` - ${child.description}` : "";
1283
+ lines.push(`- [\`${child.name}\`](${childRelative})${desc}`);
1284
+ }
1285
+ lines.push("");
1286
+ return lines;
1287
+ }
1288
+ /**
1289
+ * Renders a markdown table for positional arguments.
1290
+ */
1291
+ function renderArgsTable(args) {
1292
+ const lines = [];
1293
+ lines.push("| Argument | Type | Required | Description |");
1294
+ lines.push("| -------- | ---- | -------- | ----------- |");
1295
+ for (const arg of args) {
1296
+ const name = arg.variadic ? `${arg.name}...` : arg.name;
1297
+ const required = arg.required ? "Yes" : "No";
1298
+ const desc = escapeTableCell(formatArgDescription(arg));
1299
+ lines.push(`| \`${name}\` | ${arg.type} | ${required} | ${desc} |`);
1300
+ }
1301
+ return lines;
1302
+ }
1303
+ /**
1304
+ * Formats the description cell for an argument, including default value.
1305
+ */
1306
+ function formatArgDescription(arg) {
1307
+ const parts = [];
1308
+ if (arg.description) parts.push(arg.description);
1309
+ if (arg.default !== void 0) parts.push(`Default: \`${arg.default}\``);
1310
+ return parts.join(". ") || "-";
1311
+ }
1312
+ /**
1313
+ * Renders a markdown table for named flags.
1314
+ */
1315
+ function renderFlagsTable(flags) {
1316
+ const lines = [];
1317
+ lines.push("| Flag | Type | Required | Description |");
1318
+ lines.push("| ---- | ---- | -------- | ----------- |");
1319
+ for (const flag of flags) {
1320
+ const name = flag.spellings.map((spelling) => `\`${spelling}\``).join(", ");
1321
+ const required = flag.required ? "Yes" : "No";
1322
+ const desc = escapeTableCell(formatFlagDescription(flag));
1323
+ lines.push(`| ${name} | ${flag.type} | ${required} | ${desc} |`);
1324
+ }
1325
+ return lines;
1326
+ }
1327
+ /**
1328
+ * Formats the description cell for a flag, including default value
1329
+ * and multiplicity.
1330
+ */
1331
+ function formatFlagDescription(flag) {
1332
+ const parts = [];
1333
+ if (flag.description) parts.push(flag.description);
1334
+ if (flag.multiple) parts.push("Can be specified multiple times");
1335
+ if (flag.default !== void 0) parts.push(`Default: \`${flag.default}\``);
1336
+ return parts.join(". ") || "-";
1337
+ }
1338
+ /**
1339
+ * Renders navigation links back to the parent command and skill entrypoint.
1340
+ */
1341
+ function renderNavigation(node, root) {
1342
+ const lines = [];
1343
+ const filePath = commandFilePath(node);
1344
+ lines.push("---");
1345
+ lines.push("");
1346
+ if (node.path.length > 1) {
1347
+ const parentNode = findNode(root, node.path.slice(0, -1));
1348
+ if (parentNode) {
1349
+ const parentRelative = relativePath(filePath, commandFilePath(parentNode));
1350
+ const parentInvocation = commandInvocation(parentNode);
1351
+ lines.push(`Parent: [\`${parentInvocation}\`](${parentRelative})`);
1352
+ lines.push("");
1353
+ }
1354
+ }
1355
+ const skillRelative = relativePath(filePath, "SKILL.md");
1356
+ lines.push(`[Skill Overview](${skillRelative})`);
1357
+ lines.push("");
1358
+ return lines;
1359
+ }
1360
+ /**
1361
+ * Finds a node in the manifest tree by its full path.
1362
+ */
1363
+ function findNode(root, path) {
1364
+ if (root.path.length === path.length && root.path.every((value, i) => value === path[i])) return root;
1365
+ for (const child of root.children) {
1366
+ const found = findNode(child, path);
1367
+ if (found) return found;
1368
+ }
1369
+ }
1370
+ //#endregion
1371
+ //#region src/build.ts
1372
+ var build_exports = /* @__PURE__ */ __exportAll({
1373
+ writeSkills: () => writeSkills,
1374
+ writeSkillsFromSnapshot: () => writeSkillsFromSnapshot
1375
+ });
1376
+ /**
1377
+ * Renders generated and authored skills into a package-ready skill source.
1378
+ */
1379
+ async function writeSkills({ app, ...options }) {
1380
+ return await writeSkillSource(await app?.snapshot(), options);
1381
+ }
1382
+ /** Renders skills from a Command Snapshot prepared in this or another process. */
1383
+ async function writeSkillsFromSnapshot(snapshot, options) {
1384
+ return await writeSkillSource(snapshot, options);
1385
+ }
1386
+ async function writeSkillSource(snapshot, options) {
1387
+ if (snapshot === void 0 && (options.extras?.length ?? 0) === 0) throw new Error("Nothing to write: provide an app or at least one extra skill directory.");
1388
+ const outDir = resolve(options.outDir);
1389
+ if (basename(outDir) !== "skills") throw new Error(`Skill source outDir "${outDir}" must be named "skills".`);
1390
+ const skills = /* @__PURE__ */ new Map();
1391
+ const authoredNames = /* @__PURE__ */ new Set();
1392
+ for (const sourceDir of options.extras ?? []) {
1393
+ const resolved = resolveSourceDir(sourceDir);
1394
+ if (isWithin(outDir, resolved)) throw new Error(`Extra skill directory "${resolved}" is inside outDir "${outDir}", which is replaced on every build. Move authored skills outside the build output.`);
1395
+ const bundle = await loadBundleFiles(sourceDir);
1396
+ validateSkillName(bundle.frontmatter.name);
1397
+ if (authoredNames.has(bundle.frontmatter.name)) throw new SkillSourceConflictError(bundle.frontmatter.name);
1398
+ authoredNames.add(bundle.frontmatter.name);
1399
+ skills.set(bundle.frontmatter.name, bundle.files);
1400
+ }
1401
+ if (snapshot) {
1402
+ const generatedMeta = {
1403
+ name: options.name ?? snapshot.meta.name,
1404
+ description: options.description ?? snapshot.meta.description ?? "",
1405
+ version: options.version
1406
+ };
1407
+ if (!authoredNames.has(generatedMeta.name)) {
1408
+ validateSkillName(generatedMeta.name);
1409
+ requireSkillFrontmatter(generatedMeta, `Skill "${generatedMeta.name}"`);
1410
+ skills.set(generatedMeta.name, renderSkill(buildManifest(snapshot), generatedMeta));
1411
+ }
1412
+ }
1413
+ const cwd = resolve(".");
1414
+ if (outDir === dirname(outDir) || isWithin(outDir, cwd)) throw new Error(`Refusing to replace "${outDir}": outDir must be a dedicated directory, not the filesystem root, the working directory, or an ancestor of it.`);
1415
+ await rm(outDir, {
1416
+ recursive: true,
1417
+ force: true
1418
+ });
1419
+ const written = [];
1420
+ for (const [name, files] of skills) written.push(...(await writeFiles(join(outDir, name), files)).map((path) => join(name, path)));
1421
+ return written;
1422
+ }
1423
+ function validateSkillName(name) {
1424
+ if (!isValidSkillName(name)) throw new Error(`Invalid skill name "${name}": must be 1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.`);
1425
+ }
1426
+ async function writeFiles(baseDir, files) {
1427
+ for (const file of files) {
1428
+ const filePath = join(baseDir, file.path);
1429
+ await mkdir(dirname(filePath), { recursive: true });
1430
+ await writeFile(filePath, file.content);
1431
+ }
1432
+ return files.map((file) => file.path);
1433
+ }
1434
+ //#endregion
1435
+ export { SkillConflictError, SkillSourceConflictError, SkillSourceUnavailableError, detectInstalledAgents, getAdditionalAgents, getSkillStatus, getUniversalAgents, installSkill, isUniversalAgent, isValidSkillName, loadPackagedSkills, resolveSkillSource, skill, uninstallSkill, writeSkills, writeSkillsFromSnapshot };