226 lines
7.3 KiB
TypeScript
226 lines
7.3 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { runCli } from "../src/cli";
|
|
|
|
const repoRoot = resolve(import.meta.dir, "..");
|
|
const packageJson = JSON.parse(
|
|
readFileSync(resolve(repoRoot, "package.json"), "utf8"),
|
|
) as {
|
|
bin?: Record<string, string>;
|
|
version?: string;
|
|
};
|
|
|
|
function normalizeBinTarget(target: string | undefined) {
|
|
return target?.replace(/^[.][/\\]/, "");
|
|
}
|
|
|
|
const ansiPattern = /\x1b\[[0-9;]*m/g;
|
|
|
|
function stripAnsi(text: string) {
|
|
return text.replace(ansiPattern, "");
|
|
}
|
|
|
|
test("declares a working skvlt bin target", async () => {
|
|
expect(normalizeBinTarget(packageJson.bin?.skvlt)).toBe("src/cli.ts");
|
|
|
|
const targetPath = resolve(repoRoot, packageJson.bin!.skvlt);
|
|
expect(existsSync(targetPath)).toBe(true);
|
|
|
|
const result = Bun.spawnSync(
|
|
[process.execPath, targetPath, "backup", "--help"],
|
|
{
|
|
cwd: repoRoot,
|
|
},
|
|
);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
test("dispatches the restore command", async () => {
|
|
const result = await runCli(["restore", "--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
test("dispatches the backup command", async () => {
|
|
const result = await runCli(["backup", "--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
test("dispatches the doctor command", async () => {
|
|
const result = await runCli(["doctor", "--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
test("dispatches the completion command", async () => {
|
|
const result = await runCli(["completion", "--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
test("prints top-level help", async () => {
|
|
const result = await runCli(["--help"]);
|
|
const plainStdout = stripAnsi(result.stdout);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(plainStdout.startsWith("\n")).toBe(true);
|
|
expect(result.stdout).toContain(
|
|
"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("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).not.toContain("anthropics/skills");
|
|
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(plainStdout).toContain(
|
|
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
|
|
);
|
|
expect(
|
|
plainStdout.endsWith(
|
|
"Explore the open-source repo at https://github.com/xixu-me/skills-vault\n\n",
|
|
),
|
|
).toBe(true);
|
|
expect(result.stdout).toContain(
|
|
"Explore the open-source repo at https://github.com/xixu-me/skills-vault",
|
|
);
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
test("prints the package version", async () => {
|
|
const result = await runCli(["--version"]);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout.trim()).toBe(packageJson.version);
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
test("prints the package version as JSON", async () => {
|
|
const result = await runCli(["--json", "--version"]);
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean;
|
|
command: string;
|
|
data: { version: string };
|
|
};
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(payload).toEqual({
|
|
ok: true,
|
|
command: "version",
|
|
data: { version: packageJson.version! },
|
|
});
|
|
});
|
|
|
|
test("rejects unknown commands with guidance", async () => {
|
|
const result = await runCli(["sync"]);
|
|
const plainStderr = stripAnsi(result.stderr);
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
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.endsWith("\n\n")).toBe(true);
|
|
expect(result.stderr).toContain("Unknown command: sync");
|
|
expect(result.stderr).toContain("\x1b[1mbunx skvlt --help\x1b[0m");
|
|
});
|
|
|
|
test("returns JSON errors for unknown commands", async () => {
|
|
const result = await runCli(["sync", "--json"]);
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean;
|
|
command: string;
|
|
error: { code: string; message: string };
|
|
};
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stderr).toBe("");
|
|
expect(payload.ok).toBe(false);
|
|
expect(payload.command).toBe("sync");
|
|
expect(payload.error.code).toBe("UNKNOWN_COMMAND");
|
|
expect(payload.error.message).toContain("Unknown command: sync");
|
|
expect(result.stdout).not.toMatch(ansiPattern);
|
|
});
|
|
|
|
test("uses process argv when invoked like the CLI", async () => {
|
|
const { main } = await import("../src/cli");
|
|
const processRef = process as unknown as { argv: string[] };
|
|
const consoleRef = console as unknown as {
|
|
error: (...args: unknown[]) => void;
|
|
};
|
|
const stdoutRef = process.stdout as unknown as {
|
|
write: (chunk: string | Uint8Array) => boolean;
|
|
};
|
|
const originalArgv = processRef.argv;
|
|
const originalError = consoleRef.error;
|
|
const originalWrite = stdoutRef.write;
|
|
let stdoutText = "";
|
|
|
|
try {
|
|
processRef.argv = ["bun", "/tmp/skvlt/src/cli.ts"];
|
|
consoleRef.error = () => {};
|
|
stdoutRef.write = (chunk: string | Uint8Array) => {
|
|
stdoutText += String(chunk);
|
|
return true;
|
|
};
|
|
|
|
const exitCode = await main();
|
|
const plainStdout = stripAnsi(stdoutText);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(plainStdout).toContain(
|
|
"The CLI for backing up and restoring Agent Skills",
|
|
);
|
|
expect(plainStdout).toContain(
|
|
"Explore the open-source repo at https://github.com/xixu-me/skills-vault",
|
|
);
|
|
expect(plainStdout).not.toContain("try:");
|
|
expect(stdoutText).toContain("\x1b[38;5;102m");
|
|
expect(stdoutText).toContain("\x1b[38;5;145m");
|
|
expect(stdoutText).not.toContain("\x1b[36m");
|
|
expect(stdoutText).not.toContain("\x1b[38;5;250m");
|
|
} finally {
|
|
processRef.argv = originalArgv;
|
|
consoleRef.error = originalError;
|
|
stdoutRef.write = originalWrite;
|
|
}
|
|
});
|
|
|
|
test("shows a landing page when no command is provided", async () => {
|
|
const result = await runCli([]);
|
|
const plainStdout = stripAnsi(result.stdout);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(plainStdout.startsWith("\n")).toBe(true);
|
|
expect(plainStdout).toContain(
|
|
"The CLI for backing up and restoring Agent Skills",
|
|
);
|
|
expect(plainStdout).toContain(
|
|
"Explore the open-source repo at https://github.com/xixu-me/skills-vault",
|
|
);
|
|
expect(plainStdout).toContain(
|
|
"\n\nExplore the open-source repo at https://github.com/xixu-me/skills-vault",
|
|
);
|
|
expect(
|
|
plainStdout.endsWith(
|
|
"Explore the open-source repo at https://github.com/xixu-me/skills-vault\n\n",
|
|
),
|
|
).toBe(true);
|
|
expect(result.stdout).toContain("\x1b[38;5;102m");
|
|
expect(result.stdout).toContain("\x1b[38;5;145m");
|
|
expect(result.stdout).not.toContain("\x1b[36m");
|
|
expect(result.stdout).not.toContain("\x1b[38;5;250m");
|
|
expect(result.stdout).not.toContain("\x1b[1mUsage:\x1b[0m");
|
|
expect(result.stdout).not.toContain("Run");
|
|
});
|