Files
skills-vault/src/cli.ts
T

212 lines
6.0 KiB
TypeScript

#!/usr/bin/env bun
import packageJson from "../package.json" with { type: "json" };
import { runBackup } from "./commands/backup";
import { runCompletion } from "./commands/completion";
import { runDoctor } from "./commands/doctor";
import { runRestore } from "./commands/restore";
import {
bold,
dimGray,
textGray,
helpFooter,
helpExample,
helpHeading,
page,
stripAnsi,
} from "./internal/cli/theme";
type CliResult = {
exitCode: number;
stdout: string;
stderr: string;
errorCode?: string;
payload?: unknown;
};
type RunCliOptions = {
reportProgress?: (chunk: string) => void;
streamOutput?: boolean;
};
function printHelp(): string {
return `\n${[
"The CLI for backing up and restoring Agent Skills",
"",
helpHeading("Usage"),
" bunx skvlt <command> [options]",
"",
helpHeading("Commands"),
" backup Backup installed skills into a manifest",
" completion Print shell completion scripts",
" doctor Inspect the local environment and skill state",
" restore Restore skills from a manifest",
"",
helpHeading("Global Options"),
" -h, --help Show top-level help",
" -j, --json Print structured JSON output",
" -v, --version Print the installed package version",
"",
helpHeading("Examples"),
helpExample("bunx skvlt backup --output ./skvlt.yaml"),
helpExample("bunx skvlt completion bash"),
helpExample("bunx skvlt doctor"),
helpExample("bunx skvlt restore --all"),
helpExample("bunx skvlt restore --only-source xixu-me/skills"),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
}
function landingCommand(command: string, description: string): string {
return ` ${dimGray("$")} ${textGray(command.padEnd(28))} ${dimGray(description)}`;
}
function printLanding(): string {
return `\n${[
dimGray("The CLI for backing up and restoring Agent Skills"),
"",
landingCommand("bunx skvlt backup", "Backup installed skills"),
landingCommand("bunx skvlt restore", "Restore from skvlt.yaml"),
landingCommand("bunx skvlt doctor", "Inspect local setup"),
landingCommand("bunx skvlt completion", "Print shell completions"),
"",
`${dimGray("Explore the open-source repo at")} ${textGray("https://github.com/xixu-me/skills-vault")}\n`,
].join("\n")}`;
}
function extractGlobalFlags(argv: string[]) {
const positional: string[] = [];
let json = false;
for (const arg of argv) {
if (arg === "--json" || arg === "-j") {
json = true;
continue;
}
positional.push(arg);
}
return { argv: positional, json };
}
function toJsonResult(
command: string,
result: CliResult,
fallbackData?: unknown,
): CliResult {
const body =
result.exitCode === 0
? {
ok: true,
command,
data: result.payload ?? fallbackData ?? null,
}
: {
ok: false,
command,
error: {
code: result.errorCode ?? "CLI_ERROR",
message: stripAnsi(result.stderr || result.stdout).trim(),
},
};
return {
...result,
stdout: `${JSON.stringify(body, null, 2)}\n`,
stderr: "",
};
}
/**
* Dispatches the top-level CLI command and normalizes text or JSON output.
*/
export async function runCli(
argv: string[],
options: RunCliOptions = {},
): Promise<CliResult> {
const extracted = extractGlobalFlags(argv);
const [command] = extracted.argv;
if (!command) {
const result: CliResult = {
exitCode: 0,
stdout: `${printLanding()}\n`,
stderr: "",
payload: { text: stripAnsi(printLanding()) },
};
return extracted.json ? toJsonResult("landing", result) : result;
}
if (command === "--help" || command === "-h") {
const result: CliResult = { exitCode: 0, stdout: printHelp(), stderr: "" };
return extracted.json
? toJsonResult("help", result, { text: printHelp().trimEnd() })
: result;
}
if (command === "--version" || command === "-v") {
const result: CliResult = {
exitCode: 0,
stdout: `${packageJson.version}\n`,
stderr: "",
payload: { version: packageJson.version },
};
return extracted.json ? toJsonResult("version", result) : result;
}
if (command === "backup") {
const result = await runBackup(extracted.argv.slice(1), {
reportProgress: extracted.json ? undefined : options.reportProgress,
streamOutput: extracted.json ? false : options.streamOutput,
});
return extracted.json ? toJsonResult("backup", result) : result;
}
if (command === "doctor") {
const result = await runDoctor(extracted.argv.slice(1));
return extracted.json ? toJsonResult("doctor", result) : result;
}
if (command === "completion") {
const result = await runCompletion(extracted.argv.slice(1));
return extracted.json ? toJsonResult("completion", result) : result;
}
if (command === "restore") {
const result = await runRestore(extracted.argv.slice(1), {
reportProgress: extracted.json ? undefined : options.reportProgress,
streamOutput: extracted.json ? false : options.streamOutput,
});
return extracted.json ? toJsonResult("restore", result) : result;
}
const result: CliResult = {
exitCode: 1,
stdout: "",
stderr: page(
`Unknown command: ${command}\nRun ${bold("bunx skvlt --help")} for usage.`,
),
errorCode: "UNKNOWN_COMMAND",
};
return extracted.json ? toJsonResult(command, result) : result;
}
/**
* Executes the CLI entrypoint and writes buffered output to the process streams.
*/
export async function main(
argv: string[] = process.argv.slice(2),
): Promise<number> {
const result = await runCli(argv, {
reportProgress: (chunk) => {
process.stdout.write(chunk);
},
streamOutput: true,
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
console.error(result.stderr.trimEnd());
}
return result.exitCode;
}
if (import.meta.main) {
const exitCode = await main();
process.exit(exitCode);
}