glayguo's picture
Publish verified EvalArc 19ac9d8a5cb8
6c4b387 verified
Raw History Blame Contribute Delete
20.5 kB
#!/usr/bin/env node
import { a as resolveSource, b as resolveConfig, c as writeSourcesFile, d as originLabel, i as readSourcesFile, m as AGENTS, n as hasGit, o as sameRepository, p as findProjectRoot, r as readLock, t as SkillsAnywhereProvider, y as projectSourcesFile } from "./provider-DIndg2HA.js";
import { isAbsolute, relative, sep } from "node:path";
import { parseArgs } from "node:util";
//#region src/cli.ts
/**
* `dsh-skills-anywhere` command line: inspect what the provider would give
* dsh, manage git sources, and diagnose skipped skills — without booting dsh.
*
* @module
*/
const HELP = `dsh-skills-anywhere — your skills, anywhere.
Usage: dsh-skills-anywhere <command> [options]
Commands
list Skills the provider would publish to dsh (default)
agents Supported agents and which directories exist here
sources Configured git sources and their synced commits
add <source> Add a git source (owner/repo, owner/repo/sub/dir,
https://github.com/o/r/tree/main/dir, git URL, or path)
remove <source> Remove a git source
sync Clone or refresh every source now
doctor Explain skipped, repaired, and duplicate skills
check <files...> Check explicit Markdown files locally (strict by default)
bundle <directory> Fingerprint a skill directory or compare a reviewed manifest
mcp Serve the same skills to any MCP client over stdio
Options
--cwd <dir> Project directory (default: current directory)
--project add/remove: use <project>/.dsh/skills-anywhere.json
--ref <ref> add: branch, tag, or commit
--path <dir> add: sub-directory inside the repository
--rank <n> add: precedence rank inside dsh (lower wins)
--force sync: refresh even when pinned to a commit
--all list: include dropped duplicates
--json Machine-readable output
--lenient check: accept repairs made by the provider
--fail-on-repair check: fail if the selected mode needs any repairs
--require-pinned-sources check: fail if an external source is not pinned to an immutable revision
--against <file> bundle: compare with a saved manifest outside the directory
-h, --help Show this help
`;
async function main(argv = process.argv.slice(2)) {
let parsed;
try {
parsed = parseArgs({
args: [...argv],
allowPositionals: true,
options: {
cwd: { type: "string" },
json: {
type: "boolean",
default: false
},
all: {
type: "boolean",
default: false
},
project: {
type: "boolean",
default: false
},
force: {
type: "boolean",
default: false
},
ref: { type: "string" },
path: { type: "string" },
rank: { type: "string" },
help: {
type: "boolean",
short: "h",
default: false
},
lenient: {
type: "boolean",
default: false
},
"fail-on-repair": {
type: "boolean",
default: false
},
"require-pinned-sources": {
type: "boolean",
default: false
},
against: { type: "string" }
}
});
} catch (error) {
console.error(String(error instanceof Error ? error.message : error));
console.error(HELP);
return 2;
}
if (parsed.values.help) {
console.log(HELP);
return 0;
}
const [command = "list", ...positional] = parsed.positionals;
if (command !== "bundle" && parsed.values.against !== void 0) {
console.error("--against is only available for bundle.");
return 2;
}
if (command !== "check" && (parsed.values.lenient || parsed.values["fail-on-repair"] || parsed.values["require-pinned-sources"])) {
console.error("--lenient, --fail-on-repair and --require-pinned-sources are only available for check.");
return 2;
}
if (command === "bundle") {
if (positional.length !== 1) {
console.error("usage: dsh-skills-anywhere bundle <directory> [--against <manifest.json>] [--json]");
return 2;
}
const { bundleCommand } = await import("./bundle-cli-CiJDMO7m.js");
return bundleCommand(positional[0], {
cwd: parsed.values.cwd ?? process.cwd(),
json: parsed.values.json ?? false,
...parsed.values.against !== void 0 ? { against: parsed.values.against } : {}
});
}
if (command === "check") {
if (positional.length === 0) {
console.error("usage: dsh-skills-anywhere check <files...> [--lenient] [--fail-on-repair] [--require-pinned-sources] [--json]");
return 2;
}
const { checkFiles } = await import("./check-files-DJ37avYs.js");
const result = await checkFiles(positional, {
cwd: parsed.values.cwd ?? process.cwd(),
lenient: parsed.values.lenient ?? false,
failOnRepair: parsed.values["fail-on-repair"] ?? false,
requirePinnedSources: parsed.values["require-pinned-sources"] ?? false
});
if (parsed.values.json) console.log(JSON.stringify(result, null, 2));
else {
for (const file of result.files) {
console.log(`${file.status.toUpperCase()} ${JSON.stringify(file.path)}`);
if (file.status === "input_error") console.log(` ${JSON.stringify(file.error)}`);
else {
const selected = file.report[result.mode];
if (!selected.ok) console.log(` ${JSON.stringify(selected.reason)}`);
else for (const warning of selected.warnings) console.log(` Repair: ${JSON.stringify(warning)}`);
const { surface } = file.report;
for (const source of surface.externalSources) {
const pinning = source.pinned ? " (pinned)" : result.requirePinnedSources ? " (not pinned: required)" : " (not pinned)";
console.log(` External source: ${JSON.stringify(source.host)}${pinning}`);
}
if (surface.declaredTools.length > 0) console.log(` Declared tools: ${JSON.stringify(surface.declaredTools.join(", "))}`);
}
}
console.log(`\n${result.counts.passed} passed, ${result.counts.failed} failed, ${result.counts.inputErrors} input errors (${result.mode}).`);
console.log(result.scope);
}
return result.exitCode;
}
const cli = {
command,
positional,
cwd: parsed.values.cwd ?? process.cwd(),
json: parsed.values.json ?? false,
all: parsed.values.all ?? false,
project: parsed.values.project ?? false,
force: parsed.values.force ?? false,
...parsed.values.ref !== void 0 ? { ref: parsed.values.ref } : {},
...parsed.values.path !== void 0 ? { path: parsed.values.path } : {},
...parsed.values.rank !== void 0 ? { rank: Number(parsed.values.rank) } : {}
};
const config = resolveConfig({
watch: false,
sync: false
});
switch (command) {
case "list": return await list(cli, config);
case "agents": return agents(cli, config);
case "sources": return await sources(cli, config);
case "add": return await add(cli, config);
case "remove":
case "rm": return await remove(cli, config);
case "sync": return await sync(cli, config);
case "doctor": return await doctor(cli, config);
case "mcp": return await mcp(cli);
default:
console.error(`unknown command "${command}"\n`);
console.error(HELP);
return 2;
}
}
function logger() {
return {
info: (message) => console.error(message),
warn: (message) => console.error(message)
};
}
async function collect(cli, config) {
return (await collectWithSources(cli, config)).report;
}
/** One discovery pass plus the resolved git sources, so paths inside a source checkout can be shown repo-relative. */
async function collectWithSources(cli, config) {
const provider = new SkillsAnywhereProvider(config, logger());
try {
await provider.list({ cwd: cli.cwd });
return {
report: provider.report() ?? {
skills: [],
dropped: [],
invalid: [],
roots: [],
complete: true
},
sourceDirs: (await provider.sources(cli.cwd)).map((source) => source.dir)
};
} finally {
await provider.dispose();
}
}
/** `~`-shortened path, or the path inside its git source checkout (the FROM column already names the repo). */
function displayPath(path, config, sourceDirs) {
return sourceDirs.map((dir) => relative(dir, path)).filter((rel) => rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)).toSorted((a, b) => a.length - b.length)[0] ?? shorten(path, config.home);
}
function table(rows) {
if (rows.length === 0) return "";
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
return rows.map((row) => row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()).join("\n");
}
async function list(cli, config) {
const { report, sourceDirs } = await collectWithSources(cli, config);
if (cli.json) {
console.log(JSON.stringify({
skills: report.skills.map((skill) => ({
name: skill.name,
description: skill.description,
source: skill.source,
rank: skill.rank,
path: skill.path,
origin: skill.origin,
warnings: skill.warnings
})),
...cli.all ? { dropped: report.dropped.map((entry) => ({
name: entry.skill.name,
path: entry.skill.path,
reason: entry.reason,
winner: entry.winner.path
})) } : {},
invalid: report.invalid.map((entry) => ({
path: entry.path,
reason: entry.reason
})),
complete: report.complete
}, null, 2));
return 0;
}
if (report.skills.length === 0) console.log("No skills found outside the dsh defaults. Try: dsh-skills-anywhere add anthropics/skills");
else console.log(table([[
"NAME",
"FROM",
"PATH"
], ...report.skills.map((skill) => [
skill.name,
originLabel(skill.origin),
displayPath(skill.path, config, sourceDirs)
])]));
if (cli.all && report.dropped.length > 0) {
console.log(`\nDropped duplicates (${report.dropped.length}):`);
console.log(table(report.dropped.map((entry) => [
entry.skill.name,
entry.reason,
shorten(entry.skill.path, config.home),
`-> ${shorten(entry.winner.path, config.home)}`
])));
}
const renamed = report.skills.filter(isRenamed).length;
const repaired = report.skills.filter((skill) => skill.warnings.length > 0 && !isRenamed(skill)).length;
console.log(`\n${report.skills.length} skills` + (report.dropped.length > 0 ? `, ${report.dropped.length} duplicates hidden` : "") + (report.invalid.length > 0 ? `, ${report.invalid.length} skipped` : "") + (renamed > 0 ? `, ${renamed} renamed` : "") + (repaired > 0 ? `, ${repaired} repaired` : "") + (report.invalid.length + repaired + renamed > 0 ? " — run `dsh-skills-anywhere doctor` for details" : ""));
return 0;
}
function isRenamed(skill) {
return typeof skill.metadata.skillsAnywhere?.renamedFrom === "string";
}
async function agents(cli, config) {
const provider = new SkillsAnywhereProvider(config, logger());
try {
const roots = await provider.roots(cli.cwd);
const existing = new Set((await collect(cli, config)).roots.filter((root) => root.exists).map((root) => root.root.path));
const rows = AGENTS.map((agent) => {
const projectRoot = roots.find((root) => root.origin.kind === "agent" && root.origin.agent === agent.id && root.origin.scope === "project");
const userRoot = roots.find((root) => root.origin.kind === "agent" && root.origin.agent === agent.id && root.origin.scope === "user");
return {
id: agent.id,
label: agent.label,
project: agent.project ?? null,
user: agent.user !== void 0 ? `~/${agent.user}` : null,
present: projectRoot !== void 0 && existing.has(projectRoot.path) || userRoot !== void 0 && existing.has(userRoot.path)
};
});
if (cli.json) console.log(JSON.stringify(rows, null, 2));
else {
console.log(table([[
"AGENT",
"ID",
"PROJECT DIR",
"USER DIR",
"FOUND HERE"
], ...rows.map((row) => [
row.label,
row.id,
row.project ?? "-",
row.user ?? "-",
row.present ? "yes" : ""
])]));
console.log(`\n${rows.length} agents supported; ${rows.filter((row) => row.present).length} have a skills directory on this machine.`);
console.log("(.agents/skills and .dsh/skills are handled by the built-in dsh provider.)");
}
return 0;
} finally {
await provider.dispose();
}
}
async function sources(cli, config) {
const provider = new SkillsAnywhereProvider(config, logger());
try {
const resolved = await provider.sources(cli.cwd);
const lock = await readLock(config.lockFile);
if (cli.json) {
console.log(JSON.stringify(resolved.map((source) => ({
...source,
lock: lock[source.key] ?? null
})), null, 2));
return 0;
}
if (resolved.length === 0) {
console.log("No git sources configured. Add one with: dsh-skills-anywhere add owner/repo");
return 0;
}
console.log(table([[
"SOURCE",
"REF",
"SYNCED COMMIT",
"CACHE"
], ...resolved.map((source) => [
source.display,
source.ref ?? "(default)",
lock[source.key]?.sha.slice(0, 12) ?? "(never)",
shorten(source.dir, config.home)
])]));
console.log(`\nuser file: ${shorten(config.userSourcesFile, config.home)}`);
console.log(`project file: ${shorten(projectSourcesFile(await findProjectRoot(cli.cwd)), config.home)}`);
return 0;
} finally {
await provider.dispose();
}
}
async function targetFile(cli, config) {
return cli.project ? projectSourcesFile(await findProjectRoot(cli.cwd)) : config.userSourcesFile;
}
async function add(cli, config) {
const input = cli.positional[0];
if (input === void 0) {
console.error("usage: dsh-skills-anywhere add <source> [--ref <ref>] [--path <dir>] [--rank <n>] [--project]");
return 2;
}
const spec = {
repo: input,
...cli.ref !== void 0 ? { ref: cli.ref } : {},
...cli.path !== void 0 ? { path: cli.path } : {},
...cli.rank !== void 0 && Number.isFinite(cli.rank) ? { rank: cli.rank } : {}
};
let resolved;
try {
resolved = resolveSource(spec, config.cacheDir);
} catch (error) {
console.error(String(error instanceof Error ? error.message : error));
return 2;
}
const file = await targetFile(cli, config);
const existing = await readSourcesFile(file);
if (existing.some((entry) => sameSourcePath(entry, resolved, config.cacheDir))) {
console.log(`${resolved.display} is already in ${shorten(file, config.home)}`);
return 0;
}
await writeSourcesFile(file, [...existing, spec]);
console.log(`added ${resolved.display} to ${shorten(file, config.home)}`);
if (!await hasGit()) {
console.error("git was not found on PATH; the source will sync once git is installed.");
return 0;
}
const provider = new SkillsAnywhereProvider(config, logger());
try {
const mine = (await provider.syncAll(cli.cwd)).find((result) => result.source.id === resolved.id);
if (mine !== void 0) printSync([mine], config);
const fromSource = (await collectWith(provider, cli)).skills.filter((skill) => skill.origin.kind === "source" && skill.origin.repo === resolved.display);
console.log(`${fromSource.length} skills available from ${resolved.display}`);
return mine?.status === "failed" ? 1 : 0;
} finally {
await provider.dispose();
}
}
async function collectWith(provider, cli) {
await provider.list({ cwd: cli.cwd });
return provider.report() ?? {
skills: [],
dropped: [],
invalid: [],
roots: [],
complete: true
};
}
/** Same repository and same sub-path, comparing resolved values so `o/r/sub` and `{ repo: 'o/r', path: 'sub' }` match. */
function sameSourcePath(entry, resolved, cacheDir) {
try {
const other = resolveSource(entry, cacheDir);
return other.id === resolved.id && (other.path ?? "") === (resolved.path ?? "");
} catch {
return false;
}
}
async function remove(cli, config) {
const input = cli.positional[0];
if (input === void 0) {
console.error("usage: dsh-skills-anywhere remove <source> [--project]");
return 2;
}
const file = await targetFile(cli, config);
const existing = await readSourcesFile(file);
const remaining = existing.filter((entry) => !sameRepository(entry, input, config.cacheDir));
if (remaining.length === existing.length) {
console.error(`${input} is not listed in ${shorten(file, config.home)}`);
return 1;
}
await writeSourcesFile(file, remaining);
console.log(`removed ${input} from ${shorten(file, config.home)} (cached checkout kept under ${shorten(config.cacheDir, config.home)})`);
return 0;
}
async function sync(cli, config) {
const provider = new SkillsAnywhereProvider(config, logger());
try {
const results = await provider.syncAll(cli.cwd, { force: cli.force });
if (cli.json) console.log(JSON.stringify(results.map((result) => ({
source: result.source.display,
status: result.status,
sha: result.sha ?? null,
error: result.error ?? null
})), null, 2));
else if (results.length === 0) console.log("No git sources configured.");
else printSync(results, config);
return results.some((result) => result.status === "failed") ? 1 : 0;
} finally {
await provider.dispose();
}
}
function printSync(results, _config) {
console.log(table([[
"SOURCE",
"STATUS",
"COMMIT"
], ...results.map((result) => [
result.source.display,
result.status,
result.sha?.slice(0, 12) ?? result.error ?? ""
])]));
}
async function doctor(cli, config) {
const report = await collect(cli, config);
const git = await hasGit();
if (cli.json) {
console.log(JSON.stringify({
git,
roots: report.roots.map((root) => ({
label: root.root.label,
path: root.root.path,
exists: root.exists,
count: root.count
})),
renamed: report.skills.filter(isRenamed).map((skill) => ({
name: skill.name,
from: skill.metadata.skillsAnywhere.renamedFrom,
path: skill.path
})),
repaired: report.skills.filter((skill) => skill.warnings.length > 0 && !isRenamed(skill)).map((skill) => ({
name: skill.name,
path: skill.path,
warnings: skill.warnings
})),
invalid: report.invalid.map((entry) => ({
path: entry.path,
reason: entry.reason
})),
dropped: report.dropped.map((entry) => ({
name: entry.skill.name,
path: entry.skill.path,
reason: entry.reason,
winner: entry.winner.path
})),
complete: report.complete
}, null, 2));
return 0;
}
console.log(`git: ${git ? "available" : "NOT FOUND (git sources disabled)"}`);
console.log(`state: ${shorten(config.stateDir, config.home)}`);
const present = report.roots.filter((root) => root.exists);
console.log(`\nRoots present (${present.length} of ${report.roots.length}):`);
console.log(table(present.map((root) => [
root.root.label,
String(root.count).padStart(3),
shorten(root.root.path, config.home)
])));
const renamed = report.skills.filter(isRenamed);
if (renamed.length > 0) {
console.log(`\nRenamed to avoid collisions (${renamed.length}):`);
console.log(table(renamed.map((skill) => [
skill.metadata.skillsAnywhere.renamedFrom,
`-> ${skill.name}`,
shorten(skill.path, config.home)
])));
}
const repaired = report.skills.filter((skill) => skill.warnings.length > 0 && !isRenamed(skill));
if (repaired.length > 0) {
console.log(`\nRepaired frontmatter (${repaired.length}):`);
for (const skill of repaired) {
console.log(` ${skill.name} ${shorten(skill.path, config.home)}`);
for (const warning of skill.warnings) console.log(` - ${warning}`);
}
}
if (report.invalid.length > 0) {
console.log(`\nSkipped (${report.invalid.length}):`);
for (const entry of report.invalid) console.log(` ${shorten(entry.path, config.home)}\n - ${entry.reason}`);
}
if (report.dropped.length > 0) {
console.log(`\nHidden duplicates (${report.dropped.length}):`);
console.log(table(report.dropped.map((entry) => [
entry.skill.name,
entry.reason,
shorten(entry.skill.path, config.home),
`-> ${shorten(entry.winner.path, config.home)}`
])));
}
if (!report.complete) console.log("\nWARNING: at least one root could not be read completely; see messages above.");
return 0;
}
async function mcp(cli) {
const { runStdio } = await import("./mcp.js");
await runStdio({
cwd: cli.cwd,
config: {
sync: true,
watch: false
}
});
return 0;
}
function shorten(path, home) {
if (path === home) return "~";
const rel = relative(home, path);
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return path;
return `~${sep}${rel}`;
}
if ((() => {
const entry = process.argv[1];
return entry !== void 0 && /(?:^|[\\/])(?:dsh-skills-anywhere|cli(?:\.js|\.ts)?)$/.test(entry);
})()) main().then((code) => {
process.exitCode = code;
}, (error) => {
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
process.exitCode = 1;
});
//#endregion
export { main };