fix(cli): correct skvlt command usage

This commit is contained in:
xixu-me committed 2026-03-27 09:39:26 +08:00
1 parent 630db3d96c
commit 99dafdf804
13 files changed
+135 -95

No files matched your search

+27 -19
View File
@@ -28,24 +28,32 @@ Skills Vault exists to fill the gap described in [vercel-labs/skills#729](https:
- Check local setup quickly with `doctor`.
- Generate completion scripts for `bash`, `zsh`, and `powershell`.
## Install
Install the CLI globally with Bun:
```bash
bun add -g @xixu-me/skills-vault
```
## Quick Start
Back up your current skills:
```bash
bunx skvlt backup
skvlt backup
```
Preview what a restore would run:
```bash
bunx skvlt restore --dry-run
skvlt restore --dry-run
```
Restore everything recorded in the manifest:
```bash
bunx skvlt restore --all
skvlt restore --all
```
If you want a curated, ready-to-use manifest, see [xixu-me/skvlt](https://github.com/xixu-me/skvlt), a maintained collection of `skvlt.yaml` snapshots.
@@ -77,10 +85,10 @@ sources:
Snapshot installed skills into `skvlt.yaml`.
```bash
bunx skvlt backup
bunx skvlt backup --dry-run
bunx skvlt backup --output ./skvlt.yaml
bunx skvlt backup --project-scope --lock-file ./skills-lock.json
skvlt backup
skvlt backup --dry-run
skvlt backup --output ./skvlt.yaml
skvlt backup --project-scope --lock-file ./skills-lock.json
```
`backup` reads installed skill names, joins them with lock-file metadata, and writes a grouped manifest keyed by source.
@@ -93,10 +101,10 @@ bunx skvlt backup --project-scope --lock-file ./skills-lock.json
Install skills from a manifest.
```bash
bunx skvlt restore --all
bunx skvlt restore --only-source xixu-me/skills
bunx skvlt restore --project-scope
bunx skvlt restore --dry-run
skvlt restore --all
skvlt restore --only-source xixu-me/skills
skvlt restore --project-scope
skvlt restore --dry-run
```
By default, restore respects the scope recorded in the manifest. Use `--project-scope` to force a project install even when the manifest was created from global state.
@@ -110,8 +118,8 @@ For a curated manifest source, you can start from [xixu-me/skvlt](https://github
Inspect the local Skills Vault environment and global skill state.
```bash
bunx skvlt doctor
bunx skvlt doctor --manifest ./skvlt.yaml
skvlt doctor
skvlt doctor --manifest ./skvlt.yaml
```
`doctor` checks:
@@ -127,9 +135,9 @@ bunx skvlt doctor --manifest ./skvlt.yaml
Print shell completion scripts.
```bash
bunx skvlt completion bash
bunx skvlt completion zsh
bunx skvlt completion powershell
skvlt completion bash
skvlt completion zsh
skvlt completion powershell
```
## JSON Output
@@ -137,8 +145,8 @@ bunx skvlt completion powershell
All top-level commands support `--json` for structured output:
```bash
bunx skvlt --json doctor
bunx skvlt --json backup --dry-run
skvlt --json doctor
skvlt --json backup --dry-run
```
Successful responses include `ok`, `command`, and `data`. Failures include `ok`, `command`, and a stable `error.code` plus message.
@@ -189,7 +197,7 @@ npm publish --access public
## Troubleshooting
- If restore reports a global state mismatch, inspect `~/.agents/.skill-lock.json` and `~/.agents/skills`.
- If environment checks fail, start with `bunx skvlt doctor`.
- If environment checks fail, start with `skvlt doctor`.
- If packaging checks fail, run `bun run check` and review the tarball output from `npm pack --dry-run --json`.
For contribution, support, and security policy details, see [`CONTRIBUTING.md`](./CONTRIBUTING.md), [`SUPPORT.md`](./SUPPORT.md), and [`SECURITY.md`](./SECURITY.md).
+27 -19
View File
@@ -28,24 +28,32 @@ Skills Vault 的存在,是为了补上 [vercel-labs/skills#729](https://github
- 用 `doctor` 快速检查本地环境
- 为 `bash`、`zsh` 和 `powershell` 生成补全脚本
## 安装
使用 Bun 全局安装 CLI:
```bash
bun add -g @xixu-me/skills-vault
```
## 快速开始
备份当前已安装的 skills:
```bash
bunx skvlt backup
skvlt backup
```
预览一次 restore 将会执行什么:
```bash
bunx skvlt restore --dry-run
skvlt restore --dry-run
```
恢复 manifest 中记录的全部内容:
```bash
bunx skvlt restore --all
skvlt restore --all
```
如果你想直接使用一个精选好的 manifest,可以查看 [xixu-me/skvlt](https://github.com/xixu-me/skvlt),这是一个持续维护的 `skvlt.yaml` 集合。
@@ -77,10 +85,10 @@ sources:
把已安装的 skills 快照到 `skvlt.yaml`。
```bash
bunx skvlt backup
bunx skvlt backup --dry-run
bunx skvlt backup --output ./skvlt.yaml
bunx skvlt backup --project-scope --lock-file ./skills-lock.json
skvlt backup
skvlt backup --dry-run
skvlt backup --output ./skvlt.yaml
skvlt backup --project-scope --lock-file ./skills-lock.json
```
`backup` 会先读取已安装的 skill 名称,再结合 lock file 元数据,最终按 source 分组写出 manifest。
@@ -93,10 +101,10 @@ bunx skvlt backup --project-scope --lock-file ./skills-lock.json
从 manifest 安装 skills。
```bash
bunx skvlt restore --all
bunx skvlt restore --only-source xixu-me/skills
bunx skvlt restore --project-scope
bunx skvlt restore --dry-run
skvlt restore --all
skvlt restore --only-source xixu-me/skills
skvlt restore --project-scope
skvlt restore --dry-run
```
默认情况下,`restore` 会遵循 manifest 中记录的 scope。如果希望即使 manifest 来自全局状态,也强制恢复到项目作用域,可以使用 `--project-scope`。
@@ -110,8 +118,8 @@ bunx skvlt restore --dry-run
检查本地 Skills Vault 环境和全局 skill 状态。
```bash
bunx skvlt doctor
bunx skvlt doctor --manifest ./skvlt.yaml
skvlt doctor
skvlt doctor --manifest ./skvlt.yaml
```
`doctor` 会检查:
@@ -127,9 +135,9 @@ bunx skvlt doctor --manifest ./skvlt.yaml
输出 shell 补全脚本。
```bash
bunx skvlt completion bash
bunx skvlt completion zsh
bunx skvlt completion powershell
skvlt completion bash
skvlt completion zsh
skvlt completion powershell
```
## JSON 输出
@@ -137,8 +145,8 @@ bunx skvlt completion powershell
所有顶层命令都支持 `--json` 结构化输出:
```bash
bunx skvlt --json doctor
bunx skvlt --json backup --dry-run
skvlt --json doctor
skvlt --json backup --dry-run
```
成功时会返回 `ok`、`command` 和 `data`。失败时会返回 `ok`、`command`,以及稳定的 `error.code` 和错误消息。
@@ -189,7 +197,7 @@ npm publish --access public
## 故障排查
- 如果 restore 报告全局状态不一致,请检查 `~/.agents/.skill-lock.json` 和 `~/.agents/skills`
- 如果环境检查失败,先运行 `bunx skvlt doctor`
- 如果环境检查失败,先运行 `skvlt doctor`
- 如果打包检查失败,运行 `bun run check`,并检查 `npm pack --dry-run --json` 的输出
关于贡献、支持和安全策略,请参阅 [`CONTRIBUTING.md`](./CONTRIBUTING.md)、[`SUPPORT.md`](./SUPPORT.md) 和 [`SECURITY.md`](./SECURITY.md)。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@xixu-me/skills-vault",
"version": "1.1.1",
"version": "1.1.2",
"description": "The CLI for backing up and restoring Agent Skills",
"author": "Xi Xu",
"bin": {
+11 -11
View File
@@ -34,7 +34,7 @@ function printHelp(): string {
"The CLI for backing up and restoring Agent Skills",
"",
helpHeading("Usage"),
" bunx skvlt <command> [options]",
" skvlt <command> [options]",
"",
helpHeading("Commands"),
" backup Backup installed skills into a manifest",
@@ -48,11 +48,11 @@ function printHelp(): string {
" -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"),
helpExample("skvlt backup --output ./skvlt.yaml"),
helpExample("skvlt completion bash"),
helpExample("skvlt doctor"),
helpExample("skvlt restore --all"),
helpExample("skvlt restore --only-source xixu-me/skills"),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
}
@@ -65,10 +65,10 @@ 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"),
landingCommand("skvlt backup", "Backup installed skills"),
landingCommand("skvlt restore", "Restore from skvlt.yaml"),
landingCommand("skvlt doctor", "Inspect local setup"),
landingCommand("skvlt completion", "Print shell completions"),
"",
`${dimGray("Explore the open-source repo at")} ${textGray("https://github.com/xixu-me/skills-vault")}\n`,
].join("\n")}`;
@@ -177,7 +177,7 @@ export async function runCli(
exitCode: 1,
stdout: "",
stderr: page(
`Unknown command: ${command}\nRun ${bold("bunx skvlt --help")} for usage.`,
`Unknown command: ${command}\nRun ${bold("skvlt --help")} for usage.`,
),
errorCode: "UNKNOWN_COMMAND",
};
+10 -7
View File
@@ -48,7 +48,7 @@ function printHelp(): string {
"Backup installed skills into skvlt.yaml.",
"",
helpHeading("Usage"),
" bunx skvlt backup [options]",
" skvlt backup [options]",
"",
helpHeading("Options"),
" --output <path> Output path for skvlt.yaml",
@@ -63,10 +63,10 @@ function printHelp(): string {
" - Project scope currently requires --lock-file because the Skills CLI does not expose a discoverable project lock path here.",
"",
helpHeading("Examples"),
helpExample("bunx skvlt backup"),
helpExample(`bunx skvlt backup --output ${formatCliPath("./skvlt.yaml")}`),
helpExample("skvlt backup"),
helpExample(`skvlt backup --output ${formatCliPath("./skvlt.yaml")}`),
helpExample(
`bunx skvlt backup --project-scope --lock-file ${formatCliPath("./skills-lock.json")}`,
`skvlt backup --project-scope --lock-file ${formatCliPath("./skills-lock.json")}`,
),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
@@ -108,8 +108,8 @@ function formatBackupError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage(
message,
"bunx skvlt backup [options]",
`bunx skvlt backup --output ${formatCliPath("./skvlt.yaml")}`,
"skvlt backup [options]",
`skvlt backup --output ${formatCliPath("./skvlt.yaml")}`,
);
}
@@ -184,7 +184,10 @@ export async function runBackup(
}
appendLine(`Writing manifest to ${formatCliPath(options.outputPath)}...`);
mkdirSync(dirname(resolve(options.outputPath)), { recursive: true });
const outputDirectoryPath = dirname(resolve(options.outputPath));
if (!existsSync(outputDirectoryPath)) {
mkdirSync(outputDirectoryPath, { recursive: true });
}
writeManifestImpl(options.outputPath, manifest.yaml);
appendLine(
`Backup summary: ${manifest.totalSkills} skill(s) across ${manifest.totalSources} source(s) written to ${formatCliPath(options.outputPath)}`,
+7 -7
View File
@@ -21,18 +21,18 @@ export type CompletionRunResult = {
function printHelp(): string {
return `\n${[
"Print shell completion scripts for bunx skvlt.",
"Print shell completion scripts for skvlt.",
"",
helpHeading("Usage"),
" bunx skvlt completion <bash|zsh|powershell>",
" skvlt completion <bash|zsh|powershell>",
"",
helpHeading("Options"),
" --help Show this help",
"",
helpHeading("Examples"),
helpExample("bunx skvlt completion bash"),
helpExample("bunx skvlt completion zsh"),
helpExample("bunx skvlt completion powershell"),
helpExample("skvlt completion bash"),
helpExample("skvlt completion zsh"),
helpExample("skvlt completion powershell"),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
}
@@ -154,8 +154,8 @@ function formatCompletionError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage(
message,
"bunx skvlt completion <bash|zsh|powershell>",
"bunx skvlt completion bash",
"skvlt completion <bash|zsh|powershell>",
"skvlt completion bash",
);
}
+5 -5
View File
@@ -59,15 +59,15 @@ function printHelp(): string {
"Inspect the local Skills Vault environment and installation state.",
"",
helpHeading("Usage"),
" bunx skvlt doctor [options]",
" skvlt doctor [options]",
"",
helpHeading("Options"),
" --manifest <path> Manifest path to inspect",
" --help Show this help",
"",
helpHeading("Examples"),
helpExample("bunx skvlt doctor"),
helpExample("bunx skvlt doctor --manifest ./skvlt.yaml"),
helpExample("skvlt doctor"),
helpExample("skvlt doctor --manifest ./skvlt.yaml"),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
}
@@ -110,8 +110,8 @@ function formatDoctorError(message: string): string {
) {
return commandErrorPage(
message,
"bunx skvlt doctor [options]",
"bunx skvlt doctor --manifest ./skvlt.yaml",
"skvlt doctor [options]",
"skvlt doctor --manifest ./skvlt.yaml",
);
}
+6 -6
View File
@@ -77,7 +77,7 @@ function printHelp(): string {
"Restore skills from skvlt.yaml.",
"",
helpHeading("Usage"),
" bunx skvlt restore [options]",
" skvlt restore [options]",
"",
helpHeading("Options"),
" --manifest <path> Path to skvlt.yaml",
@@ -98,9 +98,9 @@ function printHelp(): string {
` - Global restore verifies ${formatCliPath(defaultGlobalSkillsPath)} against ${formatCliPath(defaultGlobalLockFilePath)}`,
"",
helpHeading("Examples"),
helpExample("bunx skvlt restore --only-source xixu-me/skills"),
helpExample("bunx skvlt restore --all"),
helpExample("bunx skvlt restore --project-scope --manifest ./skvlt.yaml"),
helpExample("skvlt restore --only-source xixu-me/skills"),
helpExample("skvlt restore --all"),
helpExample("skvlt restore --project-scope --manifest ./skvlt.yaml"),
helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`;
}
@@ -143,8 +143,8 @@ function formatRestoreError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage(
message,
"bunx skvlt restore [options]",
"bunx skvlt restore --all",
"skvlt restore [options]",
"skvlt restore --all",
);
}
+27 -4
View File
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { join, parse } from "node:path";
import { tmpdir } from "node:os";
import { runBackup } from "../src/commands/backup";
import { parseBackupArgs } from "../src/internal/args/parse-backup-args";
@@ -26,9 +26,9 @@ test("renders backup argument errors as a usage page", async () => {
expect(plainStderr).toContain("ERROR");
expect(plainStderr).toContain("--output requires a path");
expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("bunx skvlt backup [options]");
expect(plainStderr).toContain("skvlt backup [options]");
expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("bunx skvlt backup --output");
expect(plainStderr).toContain("skvlt backup --output");
expect(plainStderr.endsWith("\n\n")).toBe(true);
});
@@ -180,6 +180,29 @@ test("formats backup paths for the host platform in user-facing output", async (
}
});
test("does not try to recreate the filesystem root for root-level output paths", async () => {
const rootPath = parse(process.cwd()).root;
const outputPath = join(rootPath, "skvlt.yaml");
let writtenManifest: { outputPath: string; manifest: string } | undefined;
const result = await runBackup(["--output", outputPath], {
getInstalledSkillNames: async () => ["alpha"],
readLockFile: () => ({
skills: {
alpha: { source: "owner/skills" },
},
}),
writeManifest: (path, manifest) => {
writtenManifest = { outputPath: path, manifest };
},
});
expect(result.exitCode).toBe(0);
expect(writtenManifest).toBeDefined();
expect(writtenManifest?.outputPath).toBe(outputPath);
expect(writtenManifest?.manifest).toContain(' "owner/skills":');
});
test("shows help before project-scope validation", async () => {
const result = await runBackup(["--help", "--project-scope"]);
const plainStdout = result.stdout.replace(/\x1b\[[0-9;]*m/g, "");
@@ -189,7 +212,7 @@ test("shows help before project-scope validation", async () => {
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m");
expect(result.stdout).toContain("\x1b[38;5;102m$\x1b[0m");
expect(result.stdout).not.toContain("bunx skvlt backup --dry-run");
expect(result.stdout).not.toContain("skvlt backup --dry-run");
expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
);
+6 -8
View File
@@ -67,16 +67,14 @@ test("prints top-level help", async () => {
"The CLI for backing up and restoring Agent Skills",
);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("bunx skvlt <command> [options]");
expect(result.stdout).toContain("skvlt <command> [options]");
expect(result.stdout).toContain("backup");
expect(result.stdout).toContain("completion");
expect(result.stdout).toContain("doctor");
expect(result.stdout).toContain("restore");
expect(result.stdout).toContain(
"bunx skvlt restore --only-source xixu-me/skills",
);
expect(result.stdout).not.toContain("bunx skvlt backup --dry-run");
expect(result.stdout).not.toContain("bunx skvlt restore --dry-run");
expect(result.stdout).toContain("skvlt restore --only-source xixu-me/skills");
expect(result.stdout).not.toContain("skvlt backup --dry-run");
expect(result.stdout).not.toContain("skvlt restore --dry-run");
expect(result.stdout).not.toContain("anthropics/skills");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m");
@@ -128,10 +126,10 @@ test("rejects unknown commands with guidance", async () => {
expect(result.stdout).toBe("");
expect(plainStderr.startsWith("\n")).toBe(true);
expect(plainStderr).toContain("Unknown command: sync");
expect(plainStderr).toContain("Run bunx skvlt --help for usage.");
expect(plainStderr).toContain("Run skvlt --help for usage.");
expect(plainStderr.endsWith("\n\n")).toBe(true);
expect(result.stderr).toContain("Unknown command: sync");
expect(result.stderr).toContain("\x1b[1mbunx skvlt --help\x1b[0m");
expect(result.stderr).toContain("\x1b[1mskvlt --help\x1b[0m");
});
test("returns JSON errors for unknown commands", async () => {
+3 -3
View File
@@ -12,7 +12,7 @@ test("shows completion help text", async () => {
expect(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("bunx skvlt completion");
expect(result.stdout).toContain("skvlt completion");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
@@ -50,9 +50,9 @@ test("rejects unsupported shells with a stable error code", async () => {
expect(plainStderr).toContain("ERROR");
expect(plainStderr).toContain("Unsupported shell: fish");
expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("bunx skvlt completion <bash|zsh|powershell>");
expect(plainStderr).toContain("skvlt completion <bash|zsh|powershell>");
expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("bunx skvlt completion bash");
expect(plainStderr).toContain("skvlt completion bash");
expect(plainStderr.endsWith("\n\n")).toBe(true);
expect(result.stderr).toContain("\x1b[");
expect(result.stderr).toContain("ERROR");
+1 -1
View File
@@ -13,7 +13,7 @@ test("shows doctor help text", async () => {
expect(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("bunx skvlt doctor");
expect(result.stdout).toContain("skvlt doctor");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
+4 -4
View File
@@ -30,8 +30,8 @@ test("shows restore help text", async () => {
expect(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("bunx skvlt restore");
expect(result.stdout).not.toContain("bunx skvlt restore --dry-run");
expect(result.stdout).toContain("skvlt restore");
expect(result.stdout).not.toContain("skvlt restore --dry-run");
expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
);
@@ -505,8 +505,8 @@ test("renders restore argument errors as a usage page", async () => {
expect(plainStderr).toContain("ERROR");
expect(plainStderr).toContain("--concurrency requires a value");
expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("bunx skvlt restore [options]");
expect(plainStderr).toContain("skvlt restore [options]");
expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("bunx skvlt restore --all");
expect(plainStderr).toContain("skvlt restore --all");
expect(plainStderr.endsWith("\n\n")).toBe(true);
});