feat(cli): improve restore progress and command guidance
This commit is contained in:
1 parent
41dcda8cb5
commit
8ef4ad70d7
12 files changed
+140
-57
No files matched your search
@@ -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
@@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user