Skills
Package and install agent skills for AI coding assistants.
@crustjs/skills renders agent skills from a Command Snapshot. Build output is shipped with the CLI, then linked into agent directories when a user installs it.
Install
npm install @crustjs/skillsPackaged skills workflow
import { Crust as WriteCrust, defineCommand as defineWriteCommand } from "@crustjs/core";
import { skill as writeSkill, writeSkills } from "@crustjs/skills";
const writeApp = new WriteCrust("my-cli", {
description: "Manage deployments",
version: "1.2.3",
})
.add(
defineWriteCommand("deploy", { description: "Deploy the app" }, (command) =>
command.action(() => {}),
),
)
.extend(writeSkill({}));
const files = await writeSkills({
app: writeApp,
outDir: ".crust/artifacts/skills",
version: "1.2.3",
});
console.log(files);
// ["my-cli/SKILL.md", "my-cli/commands/my-cli.md", "my-cli/commands/deploy.md",
// "my-cli/commands/skills.md", "my-cli/commands/skills/update.md"]writeSkills() generates one skill from app and copies any authored extras. It replaces a dedicated output directory named skills and returns paths relative to that directory. An authored skill with the generated skill's name replaces the generated skill.
Each skill has its own directory:
.crust/artifacts/skills/
└── my-cli/
├── SKILL.md
└── commands/
├── my-cli.md
├── deploy.md
├── skills.md
└── skills/
└── update.mdUse writeSkillsFromSnapshot() when a prepared Command Snapshot is available. Omit app only when at least one authored extra is provided.
Enable discovery and installation
import { Crust, defineCommand } from "@crustjs/core";
import { skill } from "@crustjs/skills";
const deploy = defineCommand("deploy", { description: "Deploy the app" }, (command) =>
command.action(() => {}),
);
export const app = new Crust("my-cli", { description: "Manage deployments", version: "1.2.3" })
.add(deploy)
.extend(skill({}));The Extension registers my-cli skills and my-cli skills update, with skill retained as an alias. Setting command to a custom name replaces the default command without adding the alias. It also contributes an Agent skills root section with except: [skill] (the Extension factory), so it appears in help, man pages, and any other consumer, but not in the skills it generates.
The help section includes this escaped excerpt:
Agent skills:
my-cli \u2014 Manage deployments
Source: .crust/root/skills/my-cliPackaged skills are read at runtime from resolveArtifactDir("skills"); before the first build, help shows a note asking you to run crust build instead of failing. The build hook renders generated and authored skills under skills/; crust build writes them to .crust/artifacts/skills and records those files as artifacts owned by crust:skills.
Generated man pages can embed source paths resolved on the build machine. Run builds from the project root that contains the packaged skills directory to avoid absolute build-machine paths.
my-cli skills
my-cli skills --all --scope project
my-cli skills update--scope accepts project or global. With --all, scope comes from --scope, then defaultScope, then global; without --all, an interactive run prompts when neither setting is supplied.
In that prompt, deselecting an agent with an installed link removes its link. It does not merely skip a new installation.
Prop
Type
Link lifecycle and ownership
project: .agents/skills/my-cli -> ../../node_modules/my-cli/skills/my-cli
global: ~/.agents/skills/my-cli -> /usr/lib/node_modules/my-cli/skills/my-cliProject links are relative and global links are absolute. The pre-run hook repairs stale or dangling Crust-owned links unless autoUpdate: false; it does not create links that were never installed.
A link is Crust-owned when its textual target ends in skills/<name>. Real directories and foreign links conflict unless force: true is passed to low-level installation.
Supported agents
project: .agents/skills/<name>
global: ~/.agents/skills/<name>Universal targets share those directories: amp, cline, codex, cursor, gemini-cli, github-copilot, kimi-cli, opencode, pi, replit, warp, and zed. Additional targets use agent-specific locations and are found through a non-executing PATH lookup.
When the working directory is the home directory, project scope becomes global scope. Result objects include the effective scope.
Low-level link operations
await installSkill({ sourceDir, agents: ["opencode"], scope: "global" });
const status = await getSkillStatus({ name: "my-cli", sourceDir });
await uninstallSkill({ name: "my-cli" });getSkillStatus() reports linked, dangling, conflict, or absent per agent directory. uninstallSkill() removes only owned links, including dangling links. Only missing paths (ENOENT) count as absent or non-resolving; permission and other filesystem failures reject instead of being reported as missing. This applies to link inspection during installation, status checks, and uninstallation.
Prop
Type
Public workflow types
type Scope = "global" | "project";
type SkillLinkStatus = "linked" | "dangling" | "conflict" | "absent";The root exports the option, result, status, source, scope, agent, and packaged-skill types used by these functions.
Command sections
import { defineCommand as defineSectionCommand } from "@crustjs/core";
import { skill as sectionSkill } from "@crustjs/skills";
export const documentedDeploy = defineSectionCommand(
"deploy",
{
sections: [
{ title: "Safety", body: "Run preview first." },
{
title: "Agent procedure",
body: "Inspect preview output.",
only: [sectionSkill],
},
],
},
(command) => command.action(() => {}),
);Command metadata sections render as ## Title blocks in each command's file under commands/. Passing the skill factory in only limits a section to the skills renderer.
Sections with the same exact title share one heading and join their bodies. See Command Snapshot sections for shared snapshot behavior.
Name validation
isValidSkillName("deploy-guide"); // true
isValidSkillName("Deploy Guide"); // falseNames contain 1 to 64 lowercase letters, digits, and hyphens. They cannot start or end with a hyphen or contain consecutive hyphens, matching the Agent Skills specification.
Source helpers and errors
import { resolveArtifactDir } from "@crustjs/core";
import { loadPackagedSkills } from "@crustjs/skills";
const root = resolveArtifactDir("skills");
const skills = loadPackagedSkills(root);loadPackagedSkills() reads an absolute packaged skills root, validates each SKILL.md name and non-empty description, and skips directories without a SKILL.md.
SkillConflictError covers install conflicts, SkillSourceConflictError covers duplicate authored names, and SkillSourceUnavailableError means the root is missing or empty: the CLI has not been built yet.