Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions src/cli/commands/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,39 @@ describe("runAdd", () => {
).rejects.toThrow(AddError);
});

it("throws when one of multiple skills already exists (no partial writes)", async () => {
it("skips existing skills and adds the rest when adding multiple", async () => {
await writeFile(
join(projectRoot, "agents.toml"),
`version = 1\n\n[[skills]]\nname = "pdf"\nsource = "git:${repoDir}"\n`,
);

const scope = resolveScope("project", projectRoot);
const result = await runAdd({
scope,
specifier: `git:${repoDir}`,
names: ["review", "pdf"],
});

// Only "review" should be added; "pdf" was skipped
expect(result).toEqual(["review"]);
const toml = await readFile(join(projectRoot, "agents.toml"), "utf-8");
expect(toml).toContain('name = "review"');
});

it("throws when all specified skills already exist", async () => {
await writeFile(
join(projectRoot, "agents.toml"),
`version = 1\n\n[[skills]]\nname = "pdf"\nsource = "git:${repoDir}"\n\n[[skills]]\nname = "review"\nsource = "git:${repoDir}"\n`,
);

const scope = resolveScope("project", projectRoot);
await expect(
runAdd({
scope,
specifier: `git:${repoDir}`,
names: ["review", "pdf"],
names: ["pdf", "review"],
}),
).rejects.toThrow(AddError);

// "review" should NOT have been partially added
const toml = await readFile(join(projectRoot, "agents.toml"), "utf-8");
expect(toml).not.toContain('name = "review"');
});

it("throws when --all is used with names", async () => {
Expand Down Expand Up @@ -299,24 +314,39 @@ describe("runAdd (local sources)", () => {
expect(toml).not.toContain('name = "pdf"');
});

it("throws when one of multiple local skills already exists (no partial writes)", async () => {
it("skips existing local skills and adds the rest when adding multiple", async () => {
await writeFile(
join(projectRoot, "agents.toml"),
`version = 1\n\n[[skills]]\nname = "pdf"\nsource = "path:local-skills"\n`,
);

const scope = resolveScope("project", projectRoot);
const result = await runAdd({
scope,
specifier: "path:local-skills",
names: ["review", "pdf"],
});

// Only "review" should be added; "pdf" was skipped
expect(result).toEqual(["review"]);
const toml = await readFile(join(projectRoot, "agents.toml"), "utf-8");
expect(toml).toContain('name = "review"');
});

it("throws when all specified local skills already exist", async () => {
await writeFile(
join(projectRoot, "agents.toml"),
`version = 1\n\n[[skills]]\nname = "pdf"\nsource = "path:local-skills"\n\n[[skills]]\nname = "review"\nsource = "path:local-skills"\n`,
);

const scope = resolveScope("project", projectRoot);
await expect(
runAdd({
scope,
specifier: "path:local-skills",
names: ["review", "pdf"],
names: ["pdf", "review"],
}),
).rejects.toThrow(AddError);

// "review" should NOT have been partially added
const toml = await readFile(join(projectRoot, "agents.toml"), "utf-8");
expect(toml).not.toContain('name = "review"');
});
});

Expand Down
36 changes: 24 additions & 12 deletions src/cli/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,28 @@ export async function runAdd(opts: AddOptions): Promise<string | string[]> {
if (namesOverride.length === 1) {
skillName = namesOverride[0]!;
} else {
// Multiple names — check all for duplicates before writing anything
// Multiple names — skip existing, add the rest
const toAdd: string[] = [];
for (const name of namesOverride) {
if (config.skills.some((s) => s.name === name)) {
throw new AddError(
`Skill "${name}" already exists in agents.toml. Remove it first or use 'dotagents update'.`,
);
console.warn(chalk.yellow(`Skipping "${name}": already exists in agents.toml`));
} else {
toAdd.push(name);
}
}
for (const name of namesOverride) {

if (toAdd.length === 0) {
throw new AddError("All specified skills already exist in agents.toml.");
}

for (const name of toAdd) {
await addSkillToConfig(configPath, name, {
source: sourceForStorage,
...refOpts,
});
}
await runInstall({ scope });
return namesOverride;
return toAdd;
}
} else {
// No names — load SKILL.md from root for the name
Expand Down Expand Up @@ -169,22 +175,28 @@ export async function runAdd(opts: AddOptions): Promise<string | string[]> {
if (namesOverride.length === 1) {
skillName = namesOverride[0]!;
} else {
// Multiple names — check all for duplicates before writing anything
// Multiple names — skip existing, add the rest
const toAdd: string[] = [];
for (const name of namesOverride) {
if (config.skills.some((s) => s.name === name)) {
throw new AddError(
`Skill "${name}" already exists in agents.toml. Remove it first or use 'dotagents update'.`,
);
console.warn(chalk.yellow(`Skipping "${name}": already exists in agents.toml`));
} else {
toAdd.push(name);
}
}
for (const name of namesOverride) {

if (toAdd.length === 0) {
throw new AddError("All specified skills already exist in agents.toml.");
}

for (const name of toAdd) {
await addSkillToConfig(configPath, name, {
source: sourceForStorage,
...refOpts,
});
}
await runInstall({ scope });
return namesOverride;
return toAdd;
}
} else {
// Discover all skills and pick
Expand Down