From 87ed1459f02de0ceb9b9511a00eb76d81f12e558 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:58:50 -0800 Subject: [PATCH 1/3] feat(cli): Add interactive skill picker for `dotagents add` When a repo contains multiple skills and the user hasn't specified --name or --all, present an interactive multiselect prompt (via @clack/prompts) so they can pick which skills to add. Non-TTY environments retain the current error behavior. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/R6EttaJuTcw8L33gP-5t82VySXm4eadM4jnisCjQuS4 --- src/cli/commands/add.ts | 76 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/add.ts b/src/cli/commands/add.ts index f8f9e10..63adc7c 100644 --- a/src/cli/commands/add.ts +++ b/src/cli/commands/add.ts @@ -1,5 +1,6 @@ import { resolve } from "node:path"; import { parseArgs } from "node:util"; +import * as clack from "@clack/prompts"; import chalk from "chalk"; import { loadConfig } from "../../config/loader.js"; import { isWildcardDep } from "../../config/schema.js"; @@ -19,16 +20,24 @@ export class AddError extends Error { } } +export class AddCancelledError extends Error { + constructor() { + super("Cancelled"); + this.name = "AddCancelledError"; + } +} + export interface AddOptions { scope: ScopeRoot; specifier: string; ref?: string; name?: string; all?: boolean; + interactive?: boolean; } -export async function runAdd(opts: AddOptions): Promise { - const { scope, specifier, ref, name: nameOverride, all } = opts; +export async function runAdd(opts: AddOptions): Promise { + const { scope, specifier, ref, name: nameOverride, all, interactive } = opts; const { configPath } = scope; // Load config early so we can check trust before any network work @@ -48,6 +57,7 @@ export async function runAdd(opts: AddOptions): Promise { // Determine ref (flag overrides inline @ref) const effectiveRef = ref ?? parsed.ref; + const refOpts = effectiveRef ? { ref: effectiveRef } : {}; // --all: add a wildcard entry if (all) { @@ -62,7 +72,7 @@ export async function runAdd(opts: AddOptions): Promise { } await addWildcardToConfig(configPath, sourceForStorage, { - ...(effectiveRef ? { ref: effectiveRef } : {}), + ...refOpts, exclude: [], }); @@ -116,8 +126,50 @@ export async function runAdd(opts: AddOptions): Promise { } if (skills.length === 1) { skillName = skills[0]!.meta.name; + } else if (interactive) { + // Interactive TTY — let user pick from a list + const selected = await clack.multiselect({ + message: `Multiple skills found in ${sourceForStorage}. Select which to add:`, + options: skills + .sort((a, b) => a.meta.name.localeCompare(b.meta.name)) + .map((s) => ({ + label: s.meta.name, + value: s.meta.name, + hint: s.meta.description, + })), + required: true, + }); + + if (clack.isCancel(selected)) { + throw new AddCancelledError(); + } + + if (selected.length === skills.length) { + // All selected — add wildcard entry + await addWildcardToConfig(configPath, sourceForStorage, { + ...refOpts, + exclude: [], + }); + await runInstall({ scope }); + return "*"; + } + + if (selected.length === 1) { + skillName = selected[0]!; + } else { + // Multiple (but not all) selected — add each individually + for (const name of selected) { + if (config.skills.some((s) => s.name === name)) continue; + await addSkillToConfig(configPath, name, { + source: sourceForStorage, + ...refOpts, + }); + } + await runInstall({ scope }); + return selected; + } } else { - // Multiple skills found — list them and ask user to pick with --name or --all + // Non-interactive — list them and ask user to re-run with --name or --all const names = skills.map((s) => s.meta.name).sort(); throw new AddError( `Multiple skills found in ${sourceForStorage}: ${names.join(", ")}. ` + @@ -137,7 +189,7 @@ export async function runAdd(opts: AddOptions): Promise { // Add to config await addSkillToConfig(configPath, skillName, { source: sourceForStorage, - ...(effectiveRef ? { ref: effectiveRef } : {}), + ...refOpts, }); // Run install to actually fetch and place the skill @@ -170,16 +222,24 @@ export default async function add(args: string[], flags?: { user?: boolean }): P try { const scope = flags?.user ? resolveScope("user") : resolveDefaultScope(resolve(".")); - const name = await runAdd({ + const interactive = process.stdout.isTTY === true && !nameValue && !values["all"]; + const result = await runAdd({ scope, specifier, ref: values["ref"], name: nameValue, all: values["all"], + interactive, }); - const msg = name === "*" ? `Added all skills from ${specifier}` : `Added skill: ${name}`; - console.log(chalk.green(msg)); + if (result === "*") { + console.log(chalk.green(`Added all skills from ${specifier}`)); + } else if (Array.isArray(result)) { + console.log(chalk.green(`Added skills: ${result.join(", ")}`)); + } else { + console.log(chalk.green(`Added skill: ${result}`)); + } } catch (err) { + if (err instanceof AddCancelledError) return; if (err instanceof ScopeError || err instanceof AddError || err instanceof TrustError) { console.error(chalk.red(err.message)); process.exitCode = 1; From 9d0740dec1ee18f67f3b75d06c9dff97a04f70ca Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:03:29 -0800 Subject: [PATCH 2/3] fix(cli): Add duplicate wildcard check to interactive path The interactive "select all" path was missing the duplicate wildcard guard that the --all flag path has, allowing duplicate wildcard entries in agents.toml. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/pgZaHzduxghyisSik8y-K80PIfcUI2MfQN0hK5wGT8k --- src/cli/commands/add.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/commands/add.ts b/src/cli/commands/add.ts index 63adc7c..69521a1 100644 --- a/src/cli/commands/add.ts +++ b/src/cli/commands/add.ts @@ -146,6 +146,11 @@ export async function runAdd(opts: AddOptions): Promise { if (selected.length === skills.length) { // All selected — add wildcard entry + if (config.skills.some((s) => isWildcardDep(s) && sourcesMatch(s.source, sourceForStorage))) { + throw new AddError( + `A wildcard entry for "${sourceForStorage}" already exists in agents.toml.`, + ); + } await addWildcardToConfig(configPath, sourceForStorage, { ...refOpts, exclude: [], From 4dbd2542fe42ee79cd661049d51ae7a7215f4081 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:07:42 -0800 Subject: [PATCH 3/3] fix(cli): Only report actually added skills in multi-select success message Track which skills were actually added vs skipped as duplicates, and only report the added ones. Error if all selected skills already exist. Co-Authored-By: Claude --- src/cli/commands/add.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/add.ts b/src/cli/commands/add.ts index 69521a1..8788143 100644 --- a/src/cli/commands/add.ts +++ b/src/cli/commands/add.ts @@ -163,15 +163,20 @@ export async function runAdd(opts: AddOptions): Promise { skillName = selected[0]!; } else { // Multiple (but not all) selected — add each individually + const added: string[] = []; for (const name of selected) { if (config.skills.some((s) => s.name === name)) continue; await addSkillToConfig(configPath, name, { source: sourceForStorage, ...refOpts, }); + added.push(name); + } + if (added.length === 0) { + throw new AddError("All selected skills already exist in agents.toml."); } await runInstall({ scope }); - return selected; + return added; } } else { // Non-interactive — list them and ask user to re-run with --name or --all