feat(cli): improve restore progress and command guidance

This commit is contained in:
xixu-me committed 2026-03-27 01:50:45 +08:00
1 parent 41dcda8cb5
commit 8ef4ad70d7
12 files changed
+140 -57

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ coverage/
.nyc_output/ .nyc_output/
*.tgz *.tgz
*.tsbuildinfo *.tsbuildinfo
skvlt.yaml
# Bun caches and legacy lockfiles # Bun caches and legacy lockfiles
.bun/ .bun/
+11 -11
View File
@@ -34,7 +34,7 @@ function printHelp(): string {
"The CLI for backing up and restoring Agent Skills", "The CLI for backing up and restoring Agent Skills",
"", "",
helpHeading("Usage"), helpHeading("Usage"),
" skvlt <command> [options]", " bunx skvlt <command> [options]",
"", "",
helpHeading("Commands"), helpHeading("Commands"),
" backup Backup installed skills into a manifest", " backup Backup installed skills into a manifest",
@@ -48,11 +48,11 @@ function printHelp(): string {
" -v, --version Print the installed package version", " -v, --version Print the installed package version",
"", "",
helpHeading("Examples"), helpHeading("Examples"),
helpExample("skvlt backup --output ./skvlt.yaml"), helpExample("bunx skvlt backup --output ./skvlt.yaml"),
helpExample("skvlt completion bash"), helpExample("bunx skvlt completion bash"),
helpExample("skvlt doctor"), helpExample("bunx skvlt doctor"),
helpExample("skvlt restore --all"), helpExample("bunx skvlt restore --all"),
helpExample("skvlt restore --only-source xixu-me/skills"), helpExample("bunx skvlt restore --only-source xixu-me/skills"),
helpFooter("https://github.com/xixu-me/skills-vault"), helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`; ].join("\n")}`;
} }
@@ -65,10 +65,10 @@ function printLanding(): string {
return `\n${[ return `\n${[
dimGray("The CLI for backing up and restoring Agent Skills"), dimGray("The CLI for backing up and restoring Agent Skills"),
"", "",
landingCommand("skvlt backup", "Backup installed skills"), landingCommand("bunx skvlt backup", "Backup installed skills"),
landingCommand("skvlt restore", "Restore from skvlt.yaml"), landingCommand("bunx skvlt restore", "Restore from skvlt.yaml"),
landingCommand("skvlt doctor", "Inspect local setup"), landingCommand("bunx skvlt doctor", "Inspect local setup"),
landingCommand("skvlt completion", "Print shell completions"), landingCommand("bunx skvlt completion", "Print shell completions"),
"", "",
`${dimGray("Explore the open-source repo at")} ${textGray("https://github.com/xixu-me/skills-vault")}\n`, `${dimGray("Explore the open-source repo at")} ${textGray("https://github.com/xixu-me/skills-vault")}\n`,
].join("\n")}`; ].join("\n")}`;
@@ -177,7 +177,7 @@ export async function runCli(
exitCode: 1, exitCode: 1,
stdout: "", stdout: "",
stderr: page( stderr: page(
`Unknown command: ${command}\nRun ${bold("skvlt --help")} for usage.`, `Unknown command: ${command}\nRun ${bold("bunx skvlt --help")} for usage.`,
), ),
errorCode: "UNKNOWN_COMMAND", errorCode: "UNKNOWN_COMMAND",
}; };
+6 -6
View File
@@ -48,7 +48,7 @@ function printHelp(): string {
"Backup installed skills into skvlt.yaml.", "Backup installed skills into skvlt.yaml.",
"", "",
helpHeading("Usage"), helpHeading("Usage"),
" skvlt backup [options]", " bunx skvlt backup [options]",
"", "",
helpHeading("Options"), helpHeading("Options"),
" --output <path> Output path for skvlt.yaml", " --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.", " - Project scope currently requires --lock-file because the Skills CLI does not expose a discoverable project lock path here.",
"", "",
helpHeading("Examples"), helpHeading("Examples"),
helpExample("skvlt backup"), helpExample("bunx skvlt backup"),
helpExample(`skvlt backup --output ${formatCliPath("./skvlt.yaml")}`), helpExample(`bunx skvlt backup --output ${formatCliPath("./skvlt.yaml")}`),
helpExample( helpExample(
`skvlt backup --project-scope --lock-file ${formatCliPath("./skills-lock.json")}`, `bunx skvlt backup --project-scope --lock-file ${formatCliPath("./skills-lock.json")}`,
), ),
helpFooter("https://github.com/xixu-me/skills-vault"), helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`; ].join("\n")}`;
@@ -108,8 +108,8 @@ function formatBackupError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") { if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage( return commandErrorPage(
message, message,
"skvlt backup [options]", "bunx skvlt backup [options]",
`skvlt backup --output ${formatCliPath("./skvlt.yaml")}`, `bunx skvlt backup --output ${formatCliPath("./skvlt.yaml")}`,
); );
} }
+7 -7
View File
@@ -21,18 +21,18 @@ export type CompletionRunResult = {
function printHelp(): string { function printHelp(): string {
return `\n${[ return `\n${[
"Print shell completion scripts for skvlt.", "Print shell completion scripts for bunx skvlt.",
"", "",
helpHeading("Usage"), helpHeading("Usage"),
" skvlt completion <bash|zsh|powershell>", " bunx skvlt completion <bash|zsh|powershell>",
"", "",
helpHeading("Options"), helpHeading("Options"),
" --help Show this help", " --help Show this help",
"", "",
helpHeading("Examples"), helpHeading("Examples"),
helpExample("skvlt completion bash"), helpExample("bunx skvlt completion bash"),
helpExample("skvlt completion zsh"), helpExample("bunx skvlt completion zsh"),
helpExample("skvlt completion powershell"), helpExample("bunx skvlt completion powershell"),
helpFooter("https://github.com/xixu-me/skills-vault"), helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`; ].join("\n")}`;
} }
@@ -154,8 +154,8 @@ function formatCompletionError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") { if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage( return commandErrorPage(
message, message,
"skvlt completion <bash|zsh|powershell>", "bunx skvlt completion <bash|zsh|powershell>",
"skvlt completion bash", "bunx skvlt completion bash",
); );
} }
+5 -5
View File
@@ -59,15 +59,15 @@ function printHelp(): string {
"Inspect the local Skills Vault environment and installation state.", "Inspect the local Skills Vault environment and installation state.",
"", "",
helpHeading("Usage"), helpHeading("Usage"),
" skvlt doctor [options]", " bunx skvlt doctor [options]",
"", "",
helpHeading("Options"), helpHeading("Options"),
" --manifest <path> Manifest path to inspect", " --manifest <path> Manifest path to inspect",
" --help Show this help", " --help Show this help",
"", "",
helpHeading("Examples"), helpHeading("Examples"),
helpExample("skvlt doctor"), helpExample("bunx skvlt doctor"),
helpExample("skvlt doctor --manifest ./skvlt.yaml"), helpExample("bunx skvlt doctor --manifest ./skvlt.yaml"),
helpFooter("https://github.com/xixu-me/skills-vault"), helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`; ].join("\n")}`;
} }
@@ -110,8 +110,8 @@ function formatDoctorError(message: string): string {
) { ) {
return commandErrorPage( return commandErrorPage(
message, message,
"skvlt doctor [options]", "bunx skvlt doctor [options]",
"skvlt doctor --manifest ./skvlt.yaml", "bunx skvlt doctor --manifest ./skvlt.yaml",
); );
} }
+30 -11
View File
@@ -30,6 +30,7 @@ import {
helpHeading, helpHeading,
infoLine, infoLine,
page, page,
progressLine,
summaryLine, summaryLine,
} from "../internal/cli/theme"; } from "../internal/cli/theme";
import { formatCliPath } from "../internal/paths/format-cli-path"; import { formatCliPath } from "../internal/paths/format-cli-path";
@@ -76,7 +77,7 @@ function printHelp(): string {
"Restore skills from skvlt.yaml.", "Restore skills from skvlt.yaml.",
"", "",
helpHeading("Usage"), helpHeading("Usage"),
" skvlt restore [options]", " bunx skvlt restore [options]",
"", "",
helpHeading("Options"), helpHeading("Options"),
" --manifest <path> Path to skvlt.yaml", " --manifest <path> Path to skvlt.yaml",
@@ -97,9 +98,9 @@ function printHelp(): string {
` - Global restore verifies ${formatCliPath(defaultGlobalSkillsPath)} against ${formatCliPath(defaultGlobalLockFilePath)}`, ` - Global restore verifies ${formatCliPath(defaultGlobalSkillsPath)} against ${formatCliPath(defaultGlobalLockFilePath)}`,
"", "",
helpHeading("Examples"), helpHeading("Examples"),
helpExample("skvlt restore --only-source xixu-me/skills"), helpExample("bunx skvlt restore --only-source xixu-me/skills"),
helpExample("skvlt restore --all"), helpExample("bunx skvlt restore --all"),
helpExample("skvlt restore --project-scope --manifest ./skvlt.yaml"), helpExample("bunx skvlt restore --project-scope --manifest ./skvlt.yaml"),
helpFooter("https://github.com/xixu-me/skills-vault"), helpFooter("https://github.com/xixu-me/skills-vault"),
].join("\n")}`; ].join("\n")}`;
} }
@@ -142,8 +143,8 @@ function formatRestoreError(message: string, errorCode: string): string {
if (errorCode === "INVALID_ARGUMENT") { if (errorCode === "INVALID_ARGUMENT") {
return commandErrorPage( return commandErrorPage(
message, message,
"skvlt restore [options]", "bunx skvlt restore [options]",
"skvlt restore --all", "bunx skvlt restore --all",
); );
} }
@@ -218,16 +219,19 @@ async function runRestoreFromManifest(
const streamOutput = dependencies.streamOutput; const streamOutput = dependencies.streamOutput;
const reportProgress = dependencies.reportProgress; const reportProgress = dependencies.reportProgress;
let streamedOutput = false; let streamedOutput = false;
const emitStreamLine = (line: string) => {
if (!streamedOutput) {
reportProgress("\n");
streamedOutput = true;
}
reportProgress(`${line}\n`);
};
const appendLine = (line: string) => { const appendLine = (line: string) => {
const formattedLine = line.includes("summary:") const formattedLine = line.includes("summary:")
? summaryLine(line) ? summaryLine(line)
: infoLine(line); : infoLine(line);
if (streamOutput) { if (streamOutput) {
if (!streamedOutput) { emitStreamLine(formattedLine);
reportProgress("\n");
streamedOutput = true;
}
reportProgress(`${formattedLine}\n`);
return; return;
} }
@@ -302,6 +306,7 @@ async function runRestoreFromManifest(
const installedLookup = options.installAll const installedLookup = options.installAll
? null ? null
: await dependencies.getInstalledSkillNames(projectScope); : await dependencies.getInstalledSkillNames(projectScope);
const showSkillProgress = streamOutput && !options.installAll;
let pendingSkillTotal = 0; let pendingSkillTotal = 0;
let alreadyInstalledTotal = 0; let alreadyInstalledTotal = 0;
const scheduledSkillTotal = const scheduledSkillTotal =
@@ -462,6 +467,13 @@ async function runRestoreFromManifest(
appendLine( appendLine(
`Installing from ${installTasks.length} source(s) with concurrency ${concurrency} (lock-safe mode).`, `Installing from ${installTasks.length} source(s) with concurrency ${concurrency} (lock-safe mode).`,
); );
const totalProgressSkills = options.reinstallAll
? requestedSkillTotal
: pendingSkillTotal;
let completedProgressSkills = 0;
if (showSkillProgress && totalProgressSkills > 0) {
emitStreamLine(progressLine(0, totalProgressSkills));
}
let failures: ConcurrentFailure[] = []; let failures: ConcurrentFailure[] = [];
@@ -498,6 +510,13 @@ async function runRestoreFromManifest(
errors.push(errorMessage(result.stderr.trimEnd())); errors.push(errorMessage(result.stderr.trimEnd()));
} }
if (showSkillProgress && totalProgressSkills > 0) {
completedProgressSkills += task.skillsToInstall.length;
emitStreamLine(
progressLine(completedProgressSkills, totalProgressSkills),
);
}
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
const message = `Install failed for source '${task.entry.source}' with exit code ${result.exitCode}`; const message = `Install failed for source '${task.entry.source}' with exit code ${result.exitCode}`;
if (options.continueOnError) { if (options.continueOnError) {
+13
View File
@@ -103,6 +103,19 @@ export function infoLine(text: string): string {
return text; return text;
} }
export function progressLine(
completed: number,
total: number,
label: string = "skills",
): string {
const width = 10;
const safeTotal = Math.max(total, 1);
const safeCompleted = Math.min(Math.max(completed, 0), total);
const filled = Math.floor((safeCompleted / safeTotal) * width);
const bar = `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
return `${grayDark("Progress:")} ${textGray(bar)} ${dimGray(`${safeCompleted}/${total} ${label}`)}`;
}
export function summaryLine(text: string): string { export function summaryLine(text: string): string {
return grayDark(text); return grayDark(text);
} }
+3 -3
View File
@@ -26,9 +26,9 @@ test("renders backup argument errors as a usage page", async () => {
expect(plainStderr).toContain("ERROR"); expect(plainStderr).toContain("ERROR");
expect(plainStderr).toContain("--output requires a path"); expect(plainStderr).toContain("--output requires a path");
expect(plainStderr).toContain("Usage:"); expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("skvlt backup [options]"); expect(plainStderr).toContain("bunx skvlt backup [options]");
expect(plainStderr).toContain("Example:"); expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("skvlt backup --output"); expect(plainStderr).toContain("bunx skvlt backup --output");
expect(plainStderr.endsWith("\n\n")).toBe(true); expect(plainStderr.endsWith("\n\n")).toBe(true);
}); });
@@ -188,7 +188,7 @@ test("shows help before project-scope validation", async () => {
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m");
expect(result.stdout).toContain("\x1b[38;5;102m$\x1b[0m"); expect(result.stdout).toContain("\x1b[38;5;102m$\x1b[0m");
expect(result.stdout).not.toContain("skvlt backup --dry-run"); expect(result.stdout).not.toContain("bunx skvlt backup --dry-run");
expect(plainStdout).toContain( expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault", "\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
); );
+8 -6
View File
@@ -67,14 +67,16 @@ test("prints top-level help", async () => {
"The CLI for backing up and restoring Agent Skills", "The CLI for backing up and restoring Agent Skills",
); );
expect(result.stdout).toContain("Usage:"); expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("skvlt <command> [options]"); expect(result.stdout).toContain("bunx skvlt <command> [options]");
expect(result.stdout).toContain("backup"); expect(result.stdout).toContain("backup");
expect(result.stdout).toContain("completion"); expect(result.stdout).toContain("completion");
expect(result.stdout).toContain("doctor"); expect(result.stdout).toContain("doctor");
expect(result.stdout).toContain("restore"); expect(result.stdout).toContain("restore");
expect(result.stdout).toContain("skvlt restore --only-source xixu-me/skills"); expect(result.stdout).toContain(
expect(result.stdout).not.toContain("skvlt backup --dry-run"); "bunx skvlt restore --only-source xixu-me/skills",
expect(result.stdout).not.toContain("skvlt restore --dry-run"); );
expect(result.stdout).not.toContain("bunx skvlt backup --dry-run");
expect(result.stdout).not.toContain("bunx skvlt restore --dry-run");
expect(result.stdout).not.toContain("anthropics/skills"); expect(result.stdout).not.toContain("anthropics/skills");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mExamples:\x1b[0m");
@@ -126,10 +128,10 @@ test("rejects unknown commands with guidance", async () => {
expect(result.stdout).toBe(""); expect(result.stdout).toBe("");
expect(plainStderr.startsWith("\n")).toBe(true); expect(plainStderr.startsWith("\n")).toBe(true);
expect(plainStderr).toContain("Unknown command: sync"); expect(plainStderr).toContain("Unknown command: sync");
expect(plainStderr).toContain("Run skvlt --help for usage."); expect(plainStderr).toContain("Run bunx skvlt --help for usage.");
expect(plainStderr.endsWith("\n\n")).toBe(true); expect(plainStderr.endsWith("\n\n")).toBe(true);
expect(result.stderr).toContain("Unknown command: sync"); expect(result.stderr).toContain("Unknown command: sync");
expect(result.stderr).toContain("\x1b[1mskvlt --help\x1b[0m"); expect(result.stderr).toContain("\x1b[1mbunx skvlt --help\x1b[0m");
}); });
test("returns JSON errors for unknown commands", async () => { 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(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true); expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:"); expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("skvlt completion"); expect(result.stdout).toContain("bunx skvlt completion");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(plainStdout).toContain( expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault", "\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("ERROR");
expect(plainStderr).toContain("Unsupported shell: fish"); expect(plainStderr).toContain("Unsupported shell: fish");
expect(plainStderr).toContain("Usage:"); expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("skvlt completion <bash|zsh|powershell>"); expect(plainStderr).toContain("bunx skvlt completion <bash|zsh|powershell>");
expect(plainStderr).toContain("Example:"); expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("skvlt completion bash"); expect(plainStderr).toContain("bunx skvlt completion bash");
expect(plainStderr.endsWith("\n\n")).toBe(true); expect(plainStderr.endsWith("\n\n")).toBe(true);
expect(result.stderr).toContain("\x1b["); expect(result.stderr).toContain("\x1b[");
expect(result.stderr).toContain("ERROR"); 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(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true); expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:"); expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("skvlt doctor"); expect(result.stdout).toContain("bunx skvlt doctor");
expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m"); expect(result.stdout).toContain("\x1b[1mUsage:\x1b[0m");
expect(plainStdout).toContain( expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault", "\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
+52 -4
View File
@@ -30,8 +30,8 @@ test("shows restore help text", async () => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(plainStdout.startsWith("\n")).toBe(true); expect(plainStdout.startsWith("\n")).toBe(true);
expect(result.stdout).toContain("Usage:"); expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("skvlt restore"); expect(result.stdout).toContain("bunx skvlt restore");
expect(result.stdout).not.toContain("skvlt restore --dry-run"); expect(result.stdout).not.toContain("bunx skvlt restore --dry-run");
expect(plainStdout).toContain( expect(plainStdout).toContain(
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault", "\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
); );
@@ -75,6 +75,26 @@ test("prints per-source install commands during restore dry-run", async () => {
expect(result.stdout).not.toContain("github/awesome-copilot"); expect(result.stdout).not.toContain("github/awesome-copilot");
}); });
test("does not stream a skill progress bar during restore dry-run", async () => {
let streamed = "";
const result = await runRestore(
["--dry-run", "--only-source", "anthropics/skills"],
{
readManifest: () => sampleManifest,
streamOutput: true,
reportProgress: (chunk) => {
streamed += chunk;
},
},
);
const plainStream = streamed.replace(/\x1b\[[0-9;]*m/g, "");
expect(result.exitCode).toBe(0);
expect(plainStream).not.toContain("Progress:");
});
test("prints passthrough skills add options during restore dry-run", async () => { test("prints passthrough skills add options during restore dry-run", async () => {
const result = await runRestore( const result = await runRestore(
[ [
@@ -172,6 +192,34 @@ test("restores only missing skills during live execution", async () => {
]); ]);
}); });
test("streams a skill progress bar during live restore execution", async () => {
let streamed = "";
const result = await runRestore(["--manifest", "ignored.yaml"], {
readManifest: () => sampleManifest,
getInstalledSkillNames: async () => new Set(["alpha"]),
readTrackedGlobalSkillNames: async () =>
new Set(["alpha", "beta", "gamma"]),
readInstalledGlobalSkillNames: async () =>
new Set(["alpha", "beta", "gamma"]),
runBunx: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
streamOutput: true,
reportProgress: (chunk) => {
streamed += chunk;
},
});
const plainStream = streamed.replace(/\x1b\[[0-9;]*m/g, "");
expect(result.exitCode).toBe(0);
expect(plainStream).toContain(
"Installing from 2 source(s) with concurrency 1 (lock-safe mode).",
);
expect(plainStream).toContain("Progress: [----------] 0/2 skills");
expect(plainStream).toContain("Progress: [#####-----] 1/2 skills");
expect(plainStream).toContain("Progress: [##########] 2/2 skills");
});
test("passes agent and copy options through during live restore", async () => { test("passes agent and copy options through during live restore", async () => {
const commands: string[][] = []; const commands: string[][] = [];
@@ -429,8 +477,8 @@ test("renders restore argument errors as a usage page", async () => {
expect(plainStderr).toContain("ERROR"); expect(plainStderr).toContain("ERROR");
expect(plainStderr).toContain("--concurrency requires a value"); expect(plainStderr).toContain("--concurrency requires a value");
expect(plainStderr).toContain("Usage:"); expect(plainStderr).toContain("Usage:");
expect(plainStderr).toContain("skvlt restore [options]"); expect(plainStderr).toContain("bunx skvlt restore [options]");
expect(plainStderr).toContain("Example:"); expect(plainStderr).toContain("Example:");
expect(plainStderr).toContain("skvlt restore --all"); expect(plainStderr).toContain("bunx skvlt restore --all");
expect(plainStderr.endsWith("\n\n")).toBe(true); expect(plainStderr.endsWith("\n\n")).toBe(true);
}); });