212 lines
6.0 KiB
TypeScript
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);
|
|
}
|