-
-
Notifications
You must be signed in to change notification settings - Fork 3
feat(cli): Add interactive skill picker for dotagents add
#31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string> { | ||
| const { scope, specifier, ref, name: nameOverride, all } = opts; | ||
| export async function runAdd(opts: AddOptions): Promise<string | string[]> { | ||
| 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<string> { | |
|
|
||
| // 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<string> { | |
| } | ||
|
|
||
| await addWildcardToConfig(configPath, sourceForStorage, { | ||
| ...(effectiveRef ? { ref: effectiveRef } : {}), | ||
| ...refOpts, | ||
| exclude: [], | ||
| }); | ||
|
|
||
|
|
@@ -116,8 +126,60 @@ export async function runAdd(opts: AddOptions): Promise<string> { | |
| } | ||
| 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 | ||
| 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: [], | ||
| }); | ||
| await runInstall({ scope }); | ||
| return "*"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated wildcard addition logic across two code pathsLow Severity The wildcard addition logic (duplicate check, Additional Locations (1)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional — the two call sites are in different control flow contexts ( |
||
| } | ||
sentry-warden[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (selected.length === 1) { | ||
| 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 added; | ||
| } | ||
| } 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 +199,7 @@ export async function runAdd(opts: AddOptions): Promise<string> { | |
| // Add to config | ||
| await addSkillToConfig(configPath, skillName, { | ||
| source: sourceForStorage, | ||
| ...(effectiveRef ? { ref: effectiveRef } : {}), | ||
| ...refOpts, | ||
| }); | ||
|
|
||
| // Run install to actually fetch and place the skill | ||
|
|
@@ -170,16 +232,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; | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.