feat(restore): support skills add passthrough flags
This commit is contained in:
1 parent
17c27cdc84
commit
0359706be3
4 files changed
+181
-18
No files matched your search
+48
-12
@@ -39,7 +39,10 @@ Usage:
|
||||
Options:
|
||||
--manifest <path> Path to manifests/skills.yaml
|
||||
--only-source <source> Restore only one source; repeatable
|
||||
--agent <agent> Target one agent; repeatable
|
||||
--project-scope Restore to project scope instead of global
|
||||
--copy Copy files instead of symlinking to agent directories
|
||||
--all Install all skills from the selected sources
|
||||
--reinstall-all Reinstall everything instead of only missing skills
|
||||
--continue-on-error Keep going if one source fails to install
|
||||
--concurrency <count> Maximum sources to install at the same time
|
||||
@@ -116,13 +119,32 @@ function selectSources(
|
||||
function buildInstallCommand(
|
||||
source: string,
|
||||
skills: string[],
|
||||
projectScope: boolean,
|
||||
options: Pick<
|
||||
ReturnType<typeof parseRestoreArgs>,
|
||||
"agents" | "copy" | "installAll" | "projectScope"
|
||||
>,
|
||||
): string[] {
|
||||
const command = ["skills", "add", source];
|
||||
if (!projectScope) {
|
||||
if (!options.projectScope) {
|
||||
command.push("-g");
|
||||
}
|
||||
command.push("-y", "--skill", ...skills);
|
||||
|
||||
command.push("-y");
|
||||
|
||||
for (const agent of options.agents) {
|
||||
command.push("--agent", agent);
|
||||
}
|
||||
|
||||
if (options.copy) {
|
||||
command.push("--copy");
|
||||
}
|
||||
|
||||
if (options.installAll) {
|
||||
command.push("--all");
|
||||
return command;
|
||||
}
|
||||
|
||||
command.push("--skill", ...skills);
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -162,7 +184,7 @@ async function runRestoreFromManifest(
|
||||
...buildInstallCommand(
|
||||
entry.source,
|
||||
entry.skills,
|
||||
options.projectScope,
|
||||
options,
|
||||
),
|
||||
]
|
||||
.map(encodeShellArg)
|
||||
@@ -177,9 +199,9 @@ async function runRestoreFromManifest(
|
||||
};
|
||||
}
|
||||
|
||||
const installedLookup = await dependencies.getInstalledSkillNames(
|
||||
options.projectScope,
|
||||
);
|
||||
const installedLookup = options.installAll
|
||||
? null
|
||||
: await dependencies.getInstalledSkillNames(options.projectScope);
|
||||
let pendingSkillTotal = 0;
|
||||
let alreadyInstalledTotal = 0;
|
||||
const installTasks: Array<{
|
||||
@@ -189,6 +211,15 @@ async function runRestoreFromManifest(
|
||||
}> = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (options.installAll) {
|
||||
installTasks.push({
|
||||
entry,
|
||||
skillsToInstall: [],
|
||||
command: buildInstallCommand(entry.source, entry.skills, options),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.reinstallAll) {
|
||||
entry.pendingSkills = [...entry.skills];
|
||||
pendingSkillTotal += entry.count;
|
||||
@@ -198,17 +229,17 @@ async function runRestoreFromManifest(
|
||||
command: buildInstallCommand(
|
||||
entry.source,
|
||||
entry.skills,
|
||||
options.projectScope,
|
||||
options,
|
||||
),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.pendingSkills = entry.skills.filter(
|
||||
(skill) => !installedLookup.has(skill),
|
||||
(skill) => !installedLookup!.has(skill),
|
||||
);
|
||||
entry.alreadyInstalledSkills = entry.skills.filter((skill) =>
|
||||
installedLookup.has(skill),
|
||||
installedLookup!.has(skill),
|
||||
);
|
||||
pendingSkillTotal += entry.pendingSkills.length;
|
||||
alreadyInstalledTotal += entry.alreadyInstalledSkills.length;
|
||||
@@ -220,13 +251,17 @@ async function runRestoreFromManifest(
|
||||
command: buildInstallCommand(
|
||||
entry.source,
|
||||
entry.pendingSkills,
|
||||
options.projectScope,
|
||||
options,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.reinstallAll) {
|
||||
if (options.installAll) {
|
||||
lines.push(
|
||||
`Installing all skills from ${entries.length} source(s) using '${scopeLabel}' scope.`,
|
||||
);
|
||||
} else if (options.reinstallAll) {
|
||||
lines.push(
|
||||
`Reinstalling ${requestedSkillTotal} skill(s) from ${entries.length} source(s) using '${scopeLabel}' scope.`,
|
||||
);
|
||||
@@ -271,6 +306,7 @@ async function runRestoreFromManifest(
|
||||
concurrency,
|
||||
async (task) => {
|
||||
if (
|
||||
!options.installAll &&
|
||||
!options.reinstallAll &&
|
||||
task.entry.alreadyInstalledSkills.length > 0
|
||||
) {
|
||||
|
||||
@@ -3,8 +3,11 @@ import { defaultManifestPath } from "../paths/defaults";
|
||||
export type RestoreOptions = {
|
||||
manifestPath: string;
|
||||
onlySources: string[];
|
||||
agents: string[];
|
||||
projectScope: boolean;
|
||||
reinstallAll: boolean;
|
||||
installAll: boolean;
|
||||
copy: boolean;
|
||||
continueOnError: boolean;
|
||||
concurrency?: number;
|
||||
dryRun: boolean;
|
||||
@@ -28,8 +31,11 @@ export function parseRestoreArgs(argv: string[]): RestoreOptions {
|
||||
const options: RestoreOptions = {
|
||||
manifestPath: defaultManifestPath,
|
||||
onlySources: [],
|
||||
agents: [],
|
||||
projectScope: false,
|
||||
reinstallAll: false,
|
||||
installAll: false,
|
||||
copy: false,
|
||||
continueOnError: false,
|
||||
concurrency: undefined,
|
||||
dryRun: false,
|
||||
@@ -58,12 +64,27 @@ export function parseRestoreArgs(argv: string[]): RestoreOptions {
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--agent": {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith("-")) {
|
||||
throw new Error("--agent requires a value");
|
||||
}
|
||||
options.agents.push(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--project-scope":
|
||||
options.projectScope = true;
|
||||
break;
|
||||
case "--reinstall-all":
|
||||
options.reinstallAll = true;
|
||||
break;
|
||||
case "--all":
|
||||
options.installAll = true;
|
||||
break;
|
||||
case "--copy":
|
||||
options.copy = true;
|
||||
break;
|
||||
case "--continue-on-error":
|
||||
options.continueOnError = true;
|
||||
break;
|
||||
|
||||
+12
-6
@@ -16,13 +16,19 @@ test("publishes the expected package and skvlt binary metadata", () => {
|
||||
expect(existsSync(resolve(repoRoot, packageJson.bin!.skvlt))).toBe(true);
|
||||
});
|
||||
|
||||
test("documents the public skvlt CLI in the readme", () => {
|
||||
const readme = readFileSync(resolve(repoRoot, "README.md"), "utf8");
|
||||
test("uses package scripts that point at the skvlt cli entrypoint", () => {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(resolve(repoRoot, "package.json"), "utf8"),
|
||||
) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
expect(readme).toContain("# Skills Vault");
|
||||
expect(readme).toContain("`@xixu-me/skills-vault`");
|
||||
expect(readme).toContain("`skvlt`");
|
||||
expect(readme).not.toContain("bun scripts/");
|
||||
expect(packageJson.scripts?.backup).toBe("bun run ./src/cli.ts backup");
|
||||
expect(packageJson.scripts?.restore).toBe("bun run ./src/cli.ts restore");
|
||||
expect(packageJson.scripts?.format).toContain("prettier --write");
|
||||
expect(Object.values(packageJson.scripts ?? {})).not.toContain(
|
||||
expect.stringContaining("scripts/"),
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves no production files in scripts", () => {
|
||||
|
||||
@@ -40,6 +40,31 @@ test("prints per-source install commands during restore dry-run", async () => {
|
||||
expect(result.stdout).not.toContain("github/awesome-copilot");
|
||||
});
|
||||
|
||||
test("prints passthrough skills add options during restore dry-run", async () => {
|
||||
const result = await runRestore(
|
||||
[
|
||||
"--dry-run",
|
||||
"--only-source",
|
||||
"anthropics/skills",
|
||||
"--agent",
|
||||
"codex",
|
||||
"--agent",
|
||||
"claude-code",
|
||||
"--copy",
|
||||
"--all",
|
||||
],
|
||||
{
|
||||
readManifest: () => sampleManifest,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain(
|
||||
"[dry-run] bunx skills add anthropics/skills -g -y --agent codex --agent claude-code --copy --all",
|
||||
);
|
||||
expect(result.stdout).not.toContain("--skill");
|
||||
});
|
||||
|
||||
test("restores only missing skills during live execution", async () => {
|
||||
const commands: string[][] = [];
|
||||
|
||||
@@ -63,6 +88,81 @@ test("restores only missing skills during live execution", async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("passes agent and copy options through during live restore", async () => {
|
||||
const commands: string[][] = [];
|
||||
|
||||
const result = await runRestore(
|
||||
[
|
||||
"--manifest",
|
||||
"ignored.yaml",
|
||||
"--agent",
|
||||
"codex",
|
||||
"--agent",
|
||||
"claude-code",
|
||||
"--copy",
|
||||
],
|
||||
{
|
||||
readManifest: () => sampleManifest,
|
||||
getInstalledSkillNames: async () => new Set(["alpha"]),
|
||||
runBunx: async (args) => {
|
||||
commands.push(args);
|
||||
return { exitCode: 0, stdout: "", stderr: "" };
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(commands).toEqual([
|
||||
[
|
||||
"skills",
|
||||
"add",
|
||||
"anthropics/skills",
|
||||
"-g",
|
||||
"-y",
|
||||
"--agent",
|
||||
"codex",
|
||||
"--agent",
|
||||
"claude-code",
|
||||
"--copy",
|
||||
"--skill",
|
||||
"beta",
|
||||
],
|
||||
[
|
||||
"skills",
|
||||
"add",
|
||||
"github/awesome-copilot",
|
||||
"-g",
|
||||
"-y",
|
||||
"--agent",
|
||||
"codex",
|
||||
"--agent",
|
||||
"claude-code",
|
||||
"--copy",
|
||||
"--skill",
|
||||
"gamma",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("installs all source skills when --all is enabled", async () => {
|
||||
const commands: string[][] = [];
|
||||
|
||||
const result = await runRestore(["--manifest", "ignored.yaml", "--all"], {
|
||||
readManifest: () => sampleManifest,
|
||||
getInstalledSkillNames: async () => new Set(["alpha", "beta", "gamma"]),
|
||||
runBunx: async (args) => {
|
||||
commands.push(args);
|
||||
return { exitCode: 0, stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(commands).toEqual([
|
||||
["skills", "add", "anthropics/skills", "-g", "-y", "--all"],
|
||||
["skills", "add", "github/awesome-copilot", "-g", "-y", "--all"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("continues and returns success when continue-on-error is enabled", async () => {
|
||||
const result = await runRestore(
|
||||
["--manifest", "ignored.yaml", "--continue-on-error"],
|
||||
|
||||
Reference in new issue
Block a user