637 lines
28 KiB
JavaScript
637 lines
28 KiB
JavaScript
const require_chunk = require("./chunk-CZWwpsFl.cjs");
|
||
const require_skill = require("./skill-vpzqNCTw.cjs");
|
||
const require_to_error_message = require("./to-error-message-DMTSr_Bl.cjs");
|
||
const require_mcp = require("./mcp-Ccqdolho.cjs");
|
||
const require_agents_md = require("./agents-md-CdwuWt7C.cjs");
|
||
let node_fs = require("node:fs");
|
||
let node_os = require("node:os");
|
||
let node_path = require("node:path");
|
||
let node_fs_promises = require("node:fs/promises");
|
||
let commander = require("commander");
|
||
let picocolors = require("picocolors");
|
||
picocolors = require_chunk.__toESM(picocolors, 1);
|
||
let prompts = require("prompts");
|
||
prompts = require_chunk.__toESM(prompts, 1);
|
||
//#region src/cli/utils/logger.ts
|
||
const logger = {
|
||
info: (message) => {
|
||
console.log(picocolors.default.cyan("ℹ"), message);
|
||
},
|
||
success: (message) => {
|
||
console.log(picocolors.default.green("✔"), message);
|
||
},
|
||
warn: (message) => {
|
||
console.log(picocolors.default.yellow("⚠"), message);
|
||
},
|
||
error: (message) => {
|
||
console.error(picocolors.default.red("✖"), message);
|
||
}
|
||
};
|
||
//#endregion
|
||
//#region src/cli/commands/doc/resolve-agent.ts
|
||
const knownAgents = new Set(require_agents_md.listAgentsMdDescriptors().map((descriptor) => descriptor.agent));
|
||
const isAgentsMdAgent = (value) => knownAgents.has(value);
|
||
const resolveAgentsMdAgent = (value) => {
|
||
if (value === void 0) return void 0;
|
||
if (!isAgentsMdAgent(value)) throw new Error(`Unknown agent "${value}"`);
|
||
return value;
|
||
};
|
||
//#endregion
|
||
//#region src/cli/commands/doc/init.ts
|
||
const TEMPLATE = `# AGENTS.md
|
||
|
||
This document is the source of truth for AI coding agents working in this repository.
|
||
|
||
## Project overview
|
||
|
||
Short description of what this project does.
|
||
|
||
## Setup commands
|
||
|
||
\`\`\`bash
|
||
pnpm install
|
||
pnpm dev
|
||
\`\`\`
|
||
|
||
## Conventions
|
||
|
||
- Describe coding style, framework, and patterns agents should follow.
|
||
- Link to any other docs that are relevant.
|
||
|
||
## Testing
|
||
|
||
\`\`\`bash
|
||
pnpm test
|
||
\`\`\`
|
||
`;
|
||
const docInitCommand = new commander.Command("init").description("Create an AGENTS.md (or agent-specific variant) in the current directory").option("-a, --agent <agent>", "Agent to target (defaults to universal AGENTS.md)").action((options) => {
|
||
try {
|
||
const filePath = require_agents_md.resolveAgentsMdFilePath({
|
||
cwd: process.cwd(),
|
||
agent: resolveAgentsMdAgent(options.agent)
|
||
});
|
||
if ((0, node_fs.existsSync)(filePath)) {
|
||
logger.warn(`${picocolors.default.cyan(filePath)} already exists`);
|
||
return;
|
||
}
|
||
(0, node_fs.writeFileSync)(filePath, TEMPLATE, "utf-8");
|
||
logger.success(`Created ${picocolors.default.bold(filePath)}`);
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/doc/read.ts
|
||
const docReadCommand = new commander.Command("read").description("Read the current AGENTS.md (or agent-specific variant) and list its sections").option("-a, --agent <agent>", "Agent variant to read (claude-code, cursor, codex, ...)").option("-f, --file <file>", "Explicit file path (overrides --agent)").option("--json", "Output as JSON").action((options) => {
|
||
try {
|
||
const document = require_agents_md.readAgentsMd({
|
||
agent: resolveAgentsMdAgent(options.agent),
|
||
file: options.file,
|
||
cwd: process.cwd()
|
||
});
|
||
if (options.json) {
|
||
console.log(JSON.stringify({
|
||
path: document.path,
|
||
sections: document.sections.map((section) => ({
|
||
heading: section.heading,
|
||
level: section.level,
|
||
body: section.body
|
||
}))
|
||
}, null, 2));
|
||
return;
|
||
}
|
||
if (!document.content) {
|
||
logger.warn(`No file at ${picocolors.default.cyan(document.path)}`);
|
||
return;
|
||
}
|
||
logger.info(picocolors.default.bold(document.path));
|
||
for (const section of document.sections) {
|
||
const prefix = "#".repeat(section.level);
|
||
console.log(` ${picocolors.default.dim(prefix)} ${section.heading}`);
|
||
}
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/doc/remove-section.ts
|
||
const docRemoveSectionCommand = new commander.Command("remove-section").alias("rm-section").description("Remove a section from an AGENTS.md file").argument("<heading>", "Section heading to remove").option("-a, --agent <agent>", "Agent variant to target").option("-f, --file <file>", "Explicit file path (overrides --agent)").action((heading, options) => {
|
||
try {
|
||
if (require_agents_md.removeAgentsMdSection({
|
||
heading,
|
||
agent: resolveAgentsMdAgent(options.agent),
|
||
file: options.file,
|
||
cwd: process.cwd()
|
||
})) logger.success(`Removed ${picocolors.default.bold(heading)}`);
|
||
else logger.warn(`Section ${picocolors.default.bold(heading)} not found`);
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/doc/set-section.ts
|
||
const MIN_HEADING_LEVEL = 1;
|
||
const MAX_HEADING_LEVEL = 6;
|
||
const isValidPlacement = (value) => value === "append" || value === "prepend" || value === "replace";
|
||
const docSetSectionCommand = new commander.Command("set-section").description("Create or replace a section in an AGENTS.md file").argument("<heading>", "Section heading (used as the Markdown heading)").option("-a, --agent <agent>", "Agent variant to target").option("-f, --file <file>", "Explicit file path (overrides --agent)").option("--body <body>", "Inline body content").option("--body-file <path>", "Read body from a file").option("--placement <mode>", "append, prepend, or replace (default: replace)").option("--level <level>", "Heading level (1-6, default: 2)").action((heading, options) => {
|
||
try {
|
||
const agent = resolveAgentsMdAgent(options.agent);
|
||
const body = options.body ?? (options.bodyFile ? (0, node_fs.readFileSync)(options.bodyFile, "utf-8") : "");
|
||
if (!body.trim()) throw new Error("Body is empty. Provide --body or --body-file with content.");
|
||
const placement = options.placement ?? "replace";
|
||
if (!isValidPlacement(placement)) throw new Error(`Invalid placement "${placement}" (expected: append, prepend, replace)`);
|
||
const level = options.level ? Number.parseInt(options.level, 10) : 2;
|
||
if (!Number.isInteger(level) || level < MIN_HEADING_LEVEL || level > MAX_HEADING_LEVEL) throw new Error(`Invalid heading level "${options.level}" (expected: ${MIN_HEADING_LEVEL}-${MAX_HEADING_LEVEL})`);
|
||
const filePath = require_agents_md.upsertAgentsMdSection({
|
||
heading,
|
||
body,
|
||
agent,
|
||
file: options.file,
|
||
placement,
|
||
level,
|
||
cwd: process.cwd()
|
||
});
|
||
logger.success(`Updated ${picocolors.default.bold(heading)} in ${picocolors.default.cyan(filePath)}`);
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/doc/symlink-claude.ts
|
||
const docSymlinkClaudeCommand = new commander.Command("symlink-claude").description("Create a CLAUDE.md → AGENTS.md symlink (migration helper)").option("--overwrite", "Replace an existing CLAUDE.md (backs it up first)").option("--backup <name>", "Backup filename if overwriting (default: CLAUDE.md.bak)").action(async (options) => {
|
||
try {
|
||
const result = await require_agents_md.symlinkClaudeToAgents({
|
||
cwd: process.cwd(),
|
||
overwrite: Boolean(options.overwrite),
|
||
backupName: options.backup
|
||
});
|
||
if (result.alreadyLinked) {
|
||
logger.info(`CLAUDE.md already points to AGENTS.md`);
|
||
return;
|
||
}
|
||
if (result.created) {
|
||
const suffix = result.backedUpTo ? ` (backed up existing to ${picocolors.default.dim(result.backedUpTo)})` : "";
|
||
logger.success(`Linked ${picocolors.default.cyan(result.claudePath)} → AGENTS.md${suffix}`);
|
||
return;
|
||
}
|
||
logger.warn(`CLAUDE.md already exists at ${picocolors.default.cyan(result.claudePath)}. Pass --overwrite to replace it.`);
|
||
process.exitCode = 1;
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/doc/index.ts
|
||
const docCommand = new commander.Command("agents-md").alias("doc").description("Manage AGENTS.md / CLAUDE.md / GEMINI.md / Cursor rules").addCommand(docInitCommand).addCommand(docReadCommand).addCommand(docSetSectionCommand).addCommand(docRemoveSectionCommand).addCommand(docSymlinkClaudeCommand);
|
||
//#endregion
|
||
//#region src/cli/utils/format-agent-list.ts
|
||
const formatAgentList = (agentList, emptyLabel = "(none)") => agentList.length === 0 ? emptyLabel : agentList.join(", ");
|
||
//#endregion
|
||
//#region src/cli/utils/parse-mcp-agent-list.ts
|
||
const parseMcpAgentList = (input) => {
|
||
if (!input || input.length === 0) return void 0;
|
||
if (input.includes("*")) return require_mcp.getMcpAgentTypes();
|
||
const resolved = [];
|
||
for (const value of input) {
|
||
const agentType = require_mcp.resolveMcpAgentAlias(value);
|
||
if (!agentType) throw new Error(`Unknown MCP agent "${value}"`);
|
||
resolved.push(agentType);
|
||
}
|
||
return resolved;
|
||
};
|
||
//#endregion
|
||
//#region src/cli/commands/mcp/add.ts
|
||
const parseKeyValueList = (entries, separator) => {
|
||
if (!entries || entries.length === 0) return {};
|
||
const result = {};
|
||
for (const entry of entries) {
|
||
const splitIndex = entry.indexOf(separator);
|
||
if (splitIndex === -1) throw new Error(`Invalid entry "${entry}": expected "${separator}" separator`);
|
||
const key = entry.slice(0, splitIndex).trim();
|
||
const value = entry.slice(splitIndex + separator.length).trim();
|
||
if (!key) throw new Error(`Invalid entry "${entry}": empty key`);
|
||
result[key] = value;
|
||
}
|
||
return result;
|
||
};
|
||
const resolveTransport = (input) => {
|
||
if (!input) return void 0;
|
||
if (input === "http" || input === "sse") return input;
|
||
throw new Error(`Unsupported transport "${input}" (expected: http, sse)`);
|
||
};
|
||
const detectCandidateAgents = (cwd, isGlobal) => isGlobal ? require_mcp.detectGloballyInstalledMcpAgents() : require_mcp.detectProjectInstalledMcpAgents(cwd);
|
||
const mcpAddCommand = new commander.Command("add").description("Add an MCP server to coding agents").argument("<source>", "Remote URL, npm package, or command line").option("-a, --agent <agents...>", "Target specific agents (use '*' for all)").option("-g, --global", "Install to user-level config instead of project").option("-t, --transport <type>", "Transport type for remote servers (http or sse)").option("--header <header...>", "HTTP header (Key: Value), repeatable").option("--env <env...>", "Env var for stdio servers (KEY=VALUE), repeatable").option("-n, --name <name>", "Server name override").option("-y, --yes", "Skip all prompts").option("--all", "Install to all supported agents").action((source, options) => {
|
||
try {
|
||
const parsed = require_mcp.parseMcpSource(source);
|
||
const cwd = process.cwd();
|
||
const isGlobal = Boolean(options.global);
|
||
let agentTypes = options.all ? require_mcp.getMcpAgentTypes() : parseMcpAgentList(options.agent);
|
||
if (!agentTypes) {
|
||
const detected = detectCandidateAgents(cwd, isGlobal);
|
||
if (detected.length === 0) {
|
||
logger.warn(`No ${isGlobal ? "global" : "project"}-installed MCP agents detected. Pass ${picocolors.default.cyan("-a <agent>")} (e.g. ${picocolors.default.cyan("-a cursor")}) or ${picocolors.default.cyan("--all")} to install.`);
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
agentTypes = detected;
|
||
logger.info(`Detected ${isGlobal ? "global" : "project"} agents: ${picocolors.default.cyan(formatAgentList(detected, "(none detected)"))}`);
|
||
}
|
||
const result = require_mcp.installMcpServer({
|
||
source,
|
||
name: options.name,
|
||
agents: agentTypes,
|
||
global: isGlobal,
|
||
cwd,
|
||
transport: resolveTransport(options.transport),
|
||
headers: parseKeyValueList(options.header, ":"),
|
||
env: parseKeyValueList(options.env, "=")
|
||
});
|
||
logger.info(`Installing ${picocolors.default.bold(result.serverName)} (${picocolors.default.cyan(parsed.type)}) to ${picocolors.default.cyan(String(result.results.length))} agent(s)`);
|
||
for (const record of result.results) if (record.success) logger.success(`${picocolors.default.cyan(record.agent)} ${picocolors.default.dim(record.path)}`);
|
||
else logger.error(`${picocolors.default.cyan(record.agent)}: ${record.error}`);
|
||
if (result.results.some((record) => !record.success)) process.exitCode = 1;
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/mcp/list.ts
|
||
const mcpListCommand = new commander.Command("list").alias("ls").description("List installed MCP servers across agents").option("-g, --global", "List global configs instead of project").option("-a, --agent <agents...>", "Filter by specific agents").option("--json", "Output as JSON").action((options) => {
|
||
try {
|
||
const entries = require_mcp.listInstalledMcpServers({
|
||
global: Boolean(options.global),
|
||
cwd: process.cwd(),
|
||
agents: parseMcpAgentList(options.agent)
|
||
});
|
||
if (options.json) {
|
||
console.log(JSON.stringify(entries, null, 2));
|
||
return;
|
||
}
|
||
if (entries.length === 0) {
|
||
logger.warn("No MCP servers installed");
|
||
return;
|
||
}
|
||
const grouped = /* @__PURE__ */ new Map();
|
||
for (const entry of entries) {
|
||
const existing = grouped.get(entry.serverName) ?? [];
|
||
existing.push(entry);
|
||
grouped.set(entry.serverName, existing);
|
||
}
|
||
for (const [serverName, group] of grouped) {
|
||
const agentLabels = group.map((record) => record.agent).join(", ");
|
||
console.log(` ${picocolors.default.bold(serverName)} ${picocolors.default.dim(`[${agentLabels}]`)}`);
|
||
const firstPath = group[0]?.path;
|
||
if (firstPath) console.log(` ${picocolors.default.dim(firstPath)}`);
|
||
}
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/mcp/remove.ts
|
||
const mcpRemoveCommand = new commander.Command("remove").alias("rm").description("Remove an MCP server from agent configs").argument("<name>", "Server name").option("-g, --global", "Remove from global scope").option("-a, --agent <agents...>", "Filter by specific agents (use '*' for all)").option("-y, --yes", "Skip confirmation prompts").action((name, options) => {
|
||
try {
|
||
const results = require_mcp.removeMcpServer({
|
||
name,
|
||
agents: parseMcpAgentList(options.agent),
|
||
global: Boolean(options.global),
|
||
cwd: process.cwd()
|
||
});
|
||
if (results.length === 0) {
|
||
logger.warn(`No agent config contained ${picocolors.default.bold(name)}`);
|
||
return;
|
||
}
|
||
for (const record of results) if (record.removed) logger.success(`${picocolors.default.cyan(record.agent)} removed ${picocolors.default.bold(name)} ${picocolors.default.dim(record.path)}`);
|
||
else logger.error(`${picocolors.default.cyan(record.agent)}: ${record.error ?? "not found"}`);
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/mcp/index.ts
|
||
const mcpCommand = new commander.Command("mcp").description("Install, list, and remove MCP servers across coding agents").addCommand(mcpAddCommand).addCommand(mcpListCommand).addCommand(mcpRemoveCommand);
|
||
//#endregion
|
||
//#region src/cli/utils/parse-skill-agent-list.ts
|
||
const parseSkillAgentList = (input) => {
|
||
if (!input || input.length === 0) return void 0;
|
||
if (input.includes("*")) return require_skill.getSkillAgentTypes();
|
||
const validated = [];
|
||
for (const value of input) {
|
||
if (!require_skill.isSkillAgentType(value)) throw new Error(`Unknown agent "${value}"`);
|
||
validated.push(value);
|
||
}
|
||
return validated;
|
||
};
|
||
//#endregion
|
||
//#region src/cli/commands/skill/add.ts
|
||
const resolveSkillFilter = (input) => {
|
||
if (!input || input.length === 0) return void 0;
|
||
if (input.includes("*")) return void 0;
|
||
return input;
|
||
};
|
||
const resolveTargetAgentsForPrompt = async (requested, yes) => {
|
||
if (requested) return requested;
|
||
const installed = await require_skill.detectInstalledSkillAgents();
|
||
const fallback = installed.length > 0 ? installed : require_skill.getUniversalSkillAgents();
|
||
if (yes || !process.stdin.isTTY) return fallback;
|
||
const rawSelected = (await (0, prompts.default)({
|
||
type: "multiselect",
|
||
name: "agents",
|
||
message: "Install to which agents?",
|
||
choices: Object.values(require_skill.skillAgents).map((agent) => ({
|
||
title: agent.displayName,
|
||
value: agent.name,
|
||
selected: fallback.includes(agent.name)
|
||
})),
|
||
min: 1
|
||
})).agents;
|
||
if (!Array.isArray(rawSelected) || rawSelected.length === 0) {
|
||
logger.warn("No agents selected, using detected defaults");
|
||
return fallback;
|
||
}
|
||
const selected = rawSelected.filter((value) => typeof value === "string" && require_skill.isSkillAgentType(value));
|
||
return selected.length > 0 ? selected : fallback;
|
||
};
|
||
const listSkillsInSource = async (source) => {
|
||
const parsed = require_skill.parseSkillSource(source);
|
||
let basePath = "";
|
||
let subpath;
|
||
let cleanup;
|
||
try {
|
||
if (parsed.type === "local") basePath = parsed.localPath ?? parsed.url;
|
||
else if (parsed.type === "url") {
|
||
basePath = await require_skill.fetchSkillManifestFromUrl(parsed.url);
|
||
cleanup = () => require_skill.cleanupTempDir(basePath);
|
||
} else if (parsed.type === "well-known") {
|
||
basePath = await require_skill.fetchWellKnownSkills(parsed.url);
|
||
cleanup = () => require_skill.cleanupTempDir(basePath);
|
||
} else {
|
||
basePath = await require_skill.cloneRepo(parsed.url, parsed.ref);
|
||
subpath = parsed.subpath;
|
||
cleanup = () => require_skill.cleanupTempDir(basePath);
|
||
}
|
||
const skills = await require_skill.discoverSkills(basePath, subpath);
|
||
const visible = parsed.skillFilter ? require_skill.filterSkillsByName(skills, [parsed.skillFilter]) : skills;
|
||
if (visible.length === 0) {
|
||
logger.warn("No SKILL.md files found in source");
|
||
return;
|
||
}
|
||
logger.info(`Found ${visible.length} skill${visible.length === 1 ? "" : "s"}`);
|
||
for (const skill of visible) console.log(` ${picocolors.default.bold(skill.name)} ${picocolors.default.dim("-")} ${skill.description}`);
|
||
} finally {
|
||
await cleanup?.().catch(() => {});
|
||
}
|
||
};
|
||
const skillAddCommand = new commander.Command("add").description("Install skills from a source (local path, GitHub, or URL)").argument("<source>", "Source to install skills from").option("-a, --agent <agents...>", "Target specific agents (use '*' for all)").option("-s, --skill <skills...>", "Install specific skills by name").option("-g, --global", "Install to ~/.agents/skills instead of the project").option("--copy", "Copy files instead of symlinking").option("-y, --yes", "Skip all prompts").option("-l, --list", "List available skills in the source without installing").action(async (source, options) => {
|
||
try {
|
||
if (options.list) {
|
||
await listSkillsInSource(source);
|
||
return;
|
||
}
|
||
const targetAgents = await resolveTargetAgentsForPrompt(parseSkillAgentList(options.agent), Boolean(options.yes));
|
||
const skillFilter = resolveSkillFilter(options.skill);
|
||
const mode = options.copy ? "copy" : "symlink";
|
||
logger.info(`Installing from ${picocolors.default.cyan(source)} to ${picocolors.default.cyan(formatAgentList(targetAgents))}`);
|
||
const result = await require_skill.installSkillsFromSource({
|
||
source,
|
||
agents: targetAgents,
|
||
skills: skillFilter,
|
||
global: options.global,
|
||
mode,
|
||
cwd: process.cwd()
|
||
});
|
||
if (result.skills.length === 0) {
|
||
logger.warn("No SKILL.md files found in source");
|
||
return;
|
||
}
|
||
for (const record of result.installed) {
|
||
const fallbackSuffix = record.symlinkFailed ? picocolors.default.dim(" (copied: symlink fallback)") : "";
|
||
logger.success(`${picocolors.default.bold(record.skill)} → ${picocolors.default.cyan(record.agent)}${fallbackSuffix} ${picocolors.default.dim(record.path)}`);
|
||
}
|
||
for (const record of result.failed) logger.error(`${picocolors.default.bold(record.skill)} → ${picocolors.default.cyan(record.agent)}: ${record.error}`);
|
||
if (result.failed.length > 0) process.exitCode = 1;
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/skill/init.ts
|
||
const renderTemplate = (skillName) => `---
|
||
name: ${skillName}
|
||
description: A brief description of what this skill does and when to use it.
|
||
---
|
||
|
||
# ${skillName}
|
||
|
||
Describe what this skill does, when the agent should use it, and the instructions
|
||
the agent should follow when activated.
|
||
|
||
## When to use
|
||
|
||
Describe the trigger conditions (e.g. "after finishing a feature", "when the user
|
||
asks about X", "before committing React code").
|
||
|
||
## Instructions
|
||
|
||
1. First step
|
||
2. Second step
|
||
3. Additional steps as needed
|
||
`;
|
||
const skillInitCommand = new commander.Command("init").description("Create a new SKILL.md in the current directory").argument("[name]", "Skill name (defaults to current directory name)").action(async (nameArg) => {
|
||
try {
|
||
const cwd = process.cwd();
|
||
const skillName = nameArg || (0, node_path.basename)(cwd);
|
||
const hasExplicitName = nameArg !== void 0;
|
||
const skillDir = hasExplicitName ? (0, node_path.join)(cwd, skillName) : cwd;
|
||
const skillFile = (0, node_path.join)(skillDir, "SKILL.md");
|
||
const displayPath = hasExplicitName ? `${skillName}/SKILL.md` : "SKILL.md";
|
||
if ((0, node_fs.existsSync)(skillFile)) {
|
||
logger.warn(`Skill already exists at ${picocolors.default.cyan(displayPath)}`);
|
||
return;
|
||
}
|
||
if (hasExplicitName) await (0, node_fs_promises.mkdir)(skillDir, { recursive: true });
|
||
await (0, node_fs_promises.writeFile)(skillFile, renderTemplate(skillName), "utf-8");
|
||
logger.success(`Initialized skill ${picocolors.default.bold(skillName)}`);
|
||
console.log(` ${picocolors.default.dim("Created:")} ${displayPath}`);
|
||
console.log();
|
||
console.log(picocolors.default.dim("Next steps:"));
|
||
console.log(` 1. Edit ${picocolors.default.cyan(displayPath)} to define your skill instructions`);
|
||
console.log(` 2. Update the ${picocolors.default.cyan("name")} and ${picocolors.default.cyan("description")} frontmatter`);
|
||
console.log(` 3. Install it locally with ${picocolors.default.cyan(`agent-install skill add ./${hasExplicitName ? skillName : "."}`)}`);
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/skill/list.ts
|
||
const isUsableEntry = async (entry, fullPath) => {
|
||
if (entry.isDirectory()) return true;
|
||
if (!entry.isSymbolicLink()) return false;
|
||
try {
|
||
return (await (0, node_fs_promises.stat)(fullPath)).isDirectory();
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
const scanSkillsInDir = async (baseDir, agentType) => {
|
||
const entries = await (0, node_fs_promises.readdir)(baseDir, { withFileTypes: true }).catch(() => []);
|
||
const results = [];
|
||
for (const entry of entries) {
|
||
const skillDir = (0, node_path.join)(baseDir, entry.name);
|
||
if (!await isUsableEntry(entry, skillDir)) continue;
|
||
const skills = await require_skill.discoverSkills(skillDir);
|
||
for (const skill of skills) results.push({
|
||
skill: skill.name,
|
||
agent: agentType,
|
||
description: skill.description,
|
||
path: skill.path
|
||
});
|
||
}
|
||
return results;
|
||
};
|
||
const dirExists = (path) => {
|
||
try {
|
||
return (0, node_fs.existsSync)(path);
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
const getCanonicalDir = (isGlobal, cwd) => isGlobal ? (0, node_path.join)((0, node_os.homedir)(), require_skill.CANONICAL_SKILLS_DIR) : (0, node_path.join)(cwd, require_skill.CANONICAL_SKILLS_DIR);
|
||
const collectListEntries = async (options) => {
|
||
const { isGlobal, cwd, filter } = options;
|
||
const allEntries = [];
|
||
const visitedDirs = /* @__PURE__ */ new Set();
|
||
const visit = async (dir, agent) => {
|
||
if (visitedDirs.has(dir)) return;
|
||
visitedDirs.add(dir);
|
||
allEntries.push(...await scanSkillsInDir(dir, agent));
|
||
};
|
||
if (filter) {
|
||
for (const agentType of filter) await visit(require_skill.getSkillAgentDir(agentType, {
|
||
global: isGlobal,
|
||
cwd
|
||
}), agentType);
|
||
return allEntries;
|
||
}
|
||
await visit(getCanonicalDir(isGlobal, cwd), "universal");
|
||
const installedAgents = new Set(await require_skill.detectInstalledSkillAgents());
|
||
for (const agentType of require_skill.getSkillAgentTypes()) {
|
||
if (agentType === "universal") continue;
|
||
if (require_skill.isUniversalSkillAgent(agentType)) continue;
|
||
const baseDir = require_skill.getSkillAgentDir(agentType, {
|
||
global: isGlobal,
|
||
cwd
|
||
});
|
||
if (!installedAgents.has(agentType) && !dirExists(baseDir)) continue;
|
||
await visit(baseDir, agentType);
|
||
}
|
||
return allEntries;
|
||
};
|
||
const skillListCommand = new commander.Command("list").alias("ls").description("List installed skills").option("-g, --global", "List global skills instead of project skills").option("-a, --agent <agents...>", "Filter by specific agents").option("--json", "Output as JSON").action(async (options) => {
|
||
try {
|
||
const cwd = process.cwd();
|
||
const allEntries = await collectListEntries({
|
||
isGlobal: Boolean(options.global),
|
||
cwd,
|
||
filter: parseSkillAgentList(options.agent)
|
||
});
|
||
if (options.json) {
|
||
console.log(JSON.stringify(allEntries, null, 2));
|
||
return;
|
||
}
|
||
if (allEntries.length === 0) {
|
||
logger.warn("No installed skills found");
|
||
return;
|
||
}
|
||
const bySkill = /* @__PURE__ */ new Map();
|
||
for (const entry of allEntries) {
|
||
const existing = bySkill.get(entry.skill) ?? [];
|
||
existing.push(entry);
|
||
bySkill.set(entry.skill, existing);
|
||
}
|
||
const formatAgent = (agent) => agent === "universal" ? "canonical" : require_skill.skillAgents[agent].displayName;
|
||
for (const [skillName, entries] of bySkill) {
|
||
const first = entries[0];
|
||
if (!first) continue;
|
||
const agentLabels = Array.from(new Set(entries.map((entry) => formatAgent(entry.agent))));
|
||
const description = first.description || (0, node_path.basename)(first.path);
|
||
console.log(` ${picocolors.default.bold(skillName)} ${picocolors.default.dim(`[${agentLabels.join(", ")}]`)}\n ${picocolors.default.dim(description)}`);
|
||
}
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/skill/remove.ts
|
||
const skillRemoveCommand = new commander.Command("remove").alias("rm").description("Remove installed skills").argument("[skills...]", "Skills to remove (omit for interactive selection)").option("-g, --global", "Remove from global scope").option("-a, --agent <agents...>", "Remove from specific agents (use '*' for all)").option("-y, --yes", "Skip confirmation prompts").action(async (skillArgs, options) => {
|
||
try {
|
||
const cwd = process.cwd();
|
||
const isGlobal = Boolean(options.global);
|
||
const agentFilter = parseSkillAgentList(options.agent) ?? require_skill.getSkillAgentTypes();
|
||
let skillNames = skillArgs;
|
||
if (skillNames.length === 0) {
|
||
if (options.yes || !process.stdin.isTTY) {
|
||
logger.warn("No skill names provided");
|
||
return;
|
||
}
|
||
const response = await (0, prompts.default)({
|
||
type: "text",
|
||
name: "skill",
|
||
message: "Skill name to remove"
|
||
});
|
||
if (!response.skill) return;
|
||
skillNames = [response.skill];
|
||
}
|
||
const canonicalBase = isGlobal ? (0, node_path.join)((0, node_os.homedir)(), require_skill.CANONICAL_SKILLS_DIR) : (0, node_path.join)(cwd, require_skill.CANONICAL_SKILLS_DIR);
|
||
for (const rawName of skillNames) {
|
||
const sanitized = require_skill.sanitizeName(rawName);
|
||
await (0, node_fs_promises.rm)((0, node_path.join)(canonicalBase, sanitized), {
|
||
recursive: true,
|
||
force: true
|
||
}).catch(() => {});
|
||
for (const agentType of agentFilter) {
|
||
if (agentType === "universal") continue;
|
||
await (0, node_fs_promises.rm)((0, node_path.join)(require_skill.getSkillAgentDir(agentType, {
|
||
global: isGlobal,
|
||
cwd
|
||
}), sanitized), {
|
||
recursive: true,
|
||
force: true
|
||
}).catch(() => {});
|
||
}
|
||
logger.success(`Removed ${picocolors.default.bold(rawName)}`);
|
||
}
|
||
} catch (error) {
|
||
logger.error(require_to_error_message.toErrorMessage(error));
|
||
process.exitCode = 1;
|
||
}
|
||
});
|
||
//#endregion
|
||
//#region src/cli/commands/skill/index.ts
|
||
const skillCommand = new commander.Command("skill").description("Manage SKILL.md files").addCommand(skillAddCommand).addCommand(skillInitCommand).addCommand(skillListCommand).addCommand(skillRemoveCommand);
|
||
//#endregion
|
||
//#region src/cli.ts
|
||
const VERSION = "0.0.6";
|
||
process.on("SIGINT", () => process.exit(0));
|
||
process.on("SIGTERM", () => process.exit(0));
|
||
const program = new commander.Command().name("agent-install").description("Install SKILL.md files, MCP servers, and AGENTS.md guidance for any coding agent").version(VERSION, "-v, --version", "display the version number");
|
||
program.addCommand(skillCommand);
|
||
program.addCommand(mcpCommand);
|
||
program.addCommand(docCommand);
|
||
const main = async () => {
|
||
await program.parseAsync();
|
||
};
|
||
main();
|
||
//#endregion
|