diff --git a/src/cli/commands/add.test.ts b/src/cli/commands/add.test.ts index 8481de9..d5eb993 100644 --- a/src/cli/commands/add.test.ts +++ b/src/cli/commands/add.test.ts @@ -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 () => { @@ -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"'); }); }); diff --git a/src/cli/commands/add.ts b/src/cli/commands/add.ts index 7897986..933ddcc 100644 --- a/src/cli/commands/add.ts +++ b/src/cli/commands/add.ts @@ -117,22 +117,28 @@ export async function runAdd(opts: AddOptions): Promise { 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 @@ -169,22 +175,28 @@ export async function runAdd(opts: AddOptions): Promise { 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