diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 7e6d7335b..e7e76c129 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -154,10 +154,11 @@ const AI_ASSISTANT_CHECKBOX_CHOICES = [ async function promptForAgents(): Promise { let selected: AIAgentOption[] = AI_AGENT_CHECKBOX_DEFAULTS; if (Util.canPrompt()) { - const result = await InquirerWrapper.checkbox({ + const result = await InquirerWrapper.exclusiveCheckbox({ message: "Which AI agents do you want to generate skills and instructions for?", required: true, - choices: AI_AGENT_CHECKBOX_CHOICES + choices: AI_AGENT_CHECKBOX_CHOICES, + exclusiveValues: ["none"] }); selected = result as AIAgentOption[]; } @@ -167,10 +168,11 @@ async function promptForAgents(): Promise { async function promptForAssistant(): Promise { let selected: AIAssistantOption[] = AI_ASSISTANT_CHECKBOX_DEFAULTS; if (Util.canPrompt()) { - const result = await InquirerWrapper.checkbox({ + const result = await InquirerWrapper.exclusiveCheckbox({ message: "Which coding assistants should MCP servers be configured for?", required: true, - choices: AI_ASSISTANT_CHECKBOX_CHOICES + choices: AI_ASSISTANT_CHECKBOX_CHOICES, + exclusiveValues: ["none"] }); selected = result as AIAssistantOption[]; } diff --git a/packages/core/package.json b/packages/core/package.json index be8308edd..5e0a04d9f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,7 +12,9 @@ "author": "Infragistics", "license": "MIT", "dependencies": { + "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", + "@inquirer/type": "^3.0.0", "chalk": "^2.3.2", "glob": "^11.0.0", "jsonc-parser": "3.3.1", @@ -20,7 +22,6 @@ "typescript": "~5.5.4" }, "devDependencies": { - "@angular-devkit/schematics": "^22.0.0", - "@inquirer/type": "^3.0.0" + "@angular-devkit/schematics": "^22.0.0" } } diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index 68af022be..37a2585bc 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -50,6 +50,7 @@ export abstract class BasePromptSession { name: "framework", message: "Choose framework:", choices: this.getFrameworkNames(), + pageSize: 10, default: "Angular" }); @@ -708,6 +709,7 @@ type SelectOptions = Omit & { type: "select"; // TODO: Expand type: choices: any[]; + pageSize?: number; } type CheckboxOptions = Omit & { diff --git a/packages/core/prompt/ExclusiveCheckbox.ts b/packages/core/prompt/ExclusiveCheckbox.ts new file mode 100644 index 000000000..30ff21f85 --- /dev/null +++ b/packages/core/prompt/ExclusiveCheckbox.ts @@ -0,0 +1,238 @@ +import { + createPrompt, + isDownKey, + isEnterKey, + isNumberKey, + isSpaceKey, + isUpKey, + makeTheme, + useKeypress, + usePrefix, + useState, + type Status, +} from "@inquirer/core"; +import { Separator } from "@inquirer/prompts"; +import { styleText } from "node:util"; +import type { PartialDeep } from "@inquirer/type"; +import type { Theme } from "@inquirer/core"; + +export type ExclusiveCheckboxChoice = { + value: Value; + name?: string; + checked?: boolean; + disabled?: boolean | string; +}; + +type NormalizedChoice = { + value: Value; + name: string; + checked: boolean; + disabled: boolean | string; +}; + +type ExclusiveCheckboxConfig = { + message: string; + choices: ReadonlyArray | Separator>; + required?: boolean; + exclusiveValues?: readonly Value[]; + pageSize?: number; + loop?: boolean; + theme?: PartialDeep; +}; + +function normalizeChoice(choice: Value | ExclusiveCheckboxChoice | Separator): NormalizedChoice | Separator { + if (Separator.isSeparator(choice)) { + return choice; + } + + if (typeof choice === "object" && choice !== null && "value" in choice) { + const objectChoice = choice as ExclusiveCheckboxChoice; + const name = objectChoice.name ?? String(objectChoice.value); + return { + value: objectChoice.value, + name, + checked: !!objectChoice.checked, + disabled: objectChoice.disabled ?? false + }; + } + + const name = String(choice); + return { + value: choice as Value, + name, + checked: false, + disabled: false + }; +} + +function isSelectable(choice: NormalizedChoice | Separator): choice is NormalizedChoice { + return !Separator.isSeparator(choice) && !choice.disabled; +} + +function isChecked(choice: NormalizedChoice | Separator): choice is NormalizedChoice { + return !Separator.isSeparator(choice) && choice.checked; +} + +function toggleExclusiveChoice( + items: Array | Separator>, + index: number, + exclusiveValues: readonly Value[], +): Array | Separator> { + const choice = items[index]; + if (!isSelectable(choice)) { + return items; + } + + const toggledOn = !choice.checked; + const isExclusive = exclusiveValues.some(value => Object.is(value, choice.value)); + + return items.map((item, itemIndex) => { + if (Separator.isSeparator(item)) { + return item; + } + + if (itemIndex === index) { + return { ...item, checked: toggledOn }; + } + + if (toggledOn && isExclusive) { + return item.disabled ? item : { ...item, checked: false }; + } + + if (toggledOn && exclusiveValues.some(value => Object.is(value, item.value))) { + return item.disabled ? item : { ...item, checked: false }; + } + + return item; + }); +} + +function moveActiveIndex( + items: Array | Separator>, + active: number, + direction: 1 | -1, + loop: boolean, +): number { + let next = active; + for (let i = 0; i < items.length; i++) { + next += direction; + if (next < 0) { + next = loop ? items.length - 1 : 0; + } + if (next >= items.length) { + next = loop ? 0 : items.length - 1; + } + if (isSelectable(items[next])) { + return next; + } + } + return active; +} + +export function applyExclusiveToggle( + items: Array | Separator>, + index: number, + exclusiveValues: readonly Value[], +): Array | Separator> { + return toggleExclusiveChoice(items, index, exclusiveValues); +} + +export const exclusiveCheckboxTesting = { + normalizeChoice, + moveActiveIndex, + isSelectable, + isChecked, +}; + +export const exclusiveCheckbox = createPrompt>((config, done) => { + const theme = makeTheme(config.theme); + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(); + const [items, setItems] = useState | Separator>>( + config.choices.map(normalizeChoice), + ); + const firstSelectable = items.findIndex(isSelectable); + const [active, setActive] = useState(firstSelectable >= 0 ? firstSelectable : 0); + const prefix = usePrefix({ status, theme }); + const exclusiveValues = config.exclusiveValues ?? []; + + useKeypress((key) => { + if (isUpKey(key)) { + setActive(moveActiveIndex(items, active, -1, config.loop ?? true)); + return; + } + + if (isDownKey(key)) { + setActive(moveActiveIndex(items, active, 1, config.loop ?? true)); + return; + } + + if (isSpaceKey(key)) { + setItems(toggleExclusiveChoice(items, active, exclusiveValues)); + setError(undefined); + return; + } + + if (isNumberKey(key)) { + const selectedIndex = Number(key.name) - 1; + let selectableIndex = -1; + const position = items.findIndex((item) => { + if (Separator.isSeparator(item)) { + return false; + } + if (item.disabled) { + return false; + } + selectableIndex++; + return selectableIndex === selectedIndex; + }); + if (position >= 0) { + setActive(position); + setItems(toggleExclusiveChoice(items, position, exclusiveValues)); + setError(undefined); + } + return; + } + + if (isEnterKey(key)) { + const selected = items.filter(isChecked); + if (config.required && selected.length === 0) { + setError("Select at least one option."); + return; + } + setStatus("done"); + done(selected.map(choice => choice.value)); + } + }); + + const renderItem = (item: NormalizedChoice | Separator, index: number, isActive: boolean) => { + if (Separator.isSeparator(item)) { + return ` ${item.separator}`; + } + + const cursor = isActive ? ">" : " "; + const checkbox = item.checked ? "[x]" : "[ ]"; + const label = item.checked ? (item.name) : item.name; + const line = `${cursor} ${checkbox} ${label}`; + return isActive ? theme.style.highlight(line) : line; + }; + + if (status === "done") { + const answer = items.filter(isChecked).map(choice => choice.name).join(", "); + return `${prefix} ${config.message}\n${styleText("cyan", answer)}`; + } + + const renderedItems = items + .map((item, index) => renderItem(item, index, index === active)) + .join("\n"); + + const helpLine = styleText("dim", "Use ↑↓ to navigate, space to toggle, enter to submit"); + const lines = [ + `${prefix} ${config.message}`, + renderedItems, + error ? styleText("red", error) : undefined, + helpLine + ].filter(Boolean); + + return lines.join("\n"); +}); \ No newline at end of file diff --git a/packages/core/prompt/InquirerWrapper.ts b/packages/core/prompt/InquirerWrapper.ts index d96c59bd8..9ec8f341e 100644 --- a/packages/core/prompt/InquirerWrapper.ts +++ b/packages/core/prompt/InquirerWrapper.ts @@ -1,5 +1,8 @@ import { checkbox, confirm, input, select, Separator } from '@inquirer/prompts'; -import { Context } from '@inquirer/type'; +import type { Context } from '@inquirer/type'; +import type { PartialDeep } from '@inquirer/type'; +import type { Theme } from '@inquirer/core'; +import { exclusiveCheckbox, type ExclusiveCheckboxChoice } from "./ExclusiveCheckbox"; // ref - node_modules\@inquirer\input\dist\cjs\types\index.d.ts - bc for some reason this is not publicly exported type InputConfig = { @@ -19,24 +22,51 @@ type InputConfig = { type InputChoicesConfig = Omit & { choices: (string | Separator)[] | ({ value: string; name?: string; checked?: boolean } | Separator)[]; + pageSize?: number; +}; + +type ExclusiveCheckboxConfig = { + message: string; + choices: ReadonlyArray | Separator>; + required?: boolean; + exclusiveValues?: readonly string[]; + pageSize?: number; + loop?: boolean; + theme?: PartialDeep; +}; + +const promptDelegates = { + input, + select, + checkbox, + exclusiveCheckbox, + confirm, +}; + +export const inquirerWrapperTesting = { + promptDelegates, }; export class InquirerWrapper { private constructor() { } public static async input(message: InputConfig, context?: Context): Promise { - return input(message, context); + return promptDelegates.input(message, context); } public static async select(message: InputChoicesConfig, context?: Context): Promise { - return select(message, context); + return promptDelegates.select(message, context); } public static async checkbox(message: InputChoicesConfig, context?: Context): Promise { - return checkbox(message, context); + return promptDelegates.checkbox(message, context); + } + + public static async exclusiveCheckbox(message: ExclusiveCheckboxConfig, context?: Context): Promise { + return promptDelegates.exclusiveCheckbox(message, context); } public static async confirm(message: { message: string; default?: boolean }, context?: Context): Promise { - return confirm(message, context); + return promptDelegates.confirm(message, context); } } diff --git a/spec/acceptance/new-spec.ts b/spec/acceptance/new-spec.ts index 1fae2c71b..0529e2f78 100644 --- a/spec/acceptance/new-spec.ts +++ b/spec/acceptance/new-spec.ts @@ -11,7 +11,7 @@ describe("New command", () => { spyOn(console, "error"); spyOn(GoogleAnalytics, "post"); spyOn(PackageManager, "installPackages"); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"])); + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValue(Promise.resolve(["none"])); process.chdir("./output"); }); diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index 857dcc2cd..244468762 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -4,6 +4,10 @@ import * as coreDetect from "../../packages/core/util/detect-framework"; import { configureMCP, configureSkills, configureInstructions } from "../../packages/cli/lib/commands/ai-config"; import * as aiConfig from "../../packages/cli/lib/commands/ai-config"; import { addMcpServers } from "../../packages/core/util/mcp-config"; +import { applyExclusiveToggle, exclusiveCheckbox, exclusiveCheckboxTesting } from "../../packages/core/prompt/ExclusiveCheckbox"; +import { InquirerWrapper as LocalInquirerWrapper, inquirerWrapperTesting } from "../../packages/core/prompt/InquirerWrapper"; +import { Separator } from "@inquirer/prompts"; +import { PassThrough, Writable } from "stream"; const IGNITEUI_SERVER_KEY = "igniteui-cli"; const IGNITEUI_THEMING_SERVER_KEY = "igniteui-theming"; @@ -155,6 +159,257 @@ describe("Unit - ai-config command", () => { }); }); + describe("exclusive checkbox behavior", () => { + async function runExclusivePrompt(keys: string[], config: any) { + const input = new PassThrough(); + let rendered = ""; + const output = new Writable({ + write(chunk, _encoding, callback) { + rendered += chunk.toString(); + callback(); + } + }); + + const promise = exclusiveCheckbox(config, { input, output, clearPromptOnDone: true } as any); + for (const key of keys) { + input.write(key); + } + + const result = await promise; + return { result, rendered }; + } + + it("clears other selections when None is selected", () => { + const items = [ + { value: "none", name: "None", checked: false, disabled: false }, + { value: "generic", name: "Generic", checked: true, disabled: false }, + { value: "claude", name: "Claude", checked: true, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 0, ["none"]); + + expect(result[0]).toEqual(jasmine.objectContaining({ checked: true })); + expect(result[1]).toEqual(jasmine.objectContaining({ checked: false })); + expect(result[2]).toEqual(jasmine.objectContaining({ checked: false })); + }); + + it("clears None when another selection is made", () => { + const items = [ + { value: "none", name: "None", checked: true, disabled: false }, + { value: "generic", name: "Generic", checked: false, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 1, ["none"]); + + expect(result[0]).toEqual(jasmine.objectContaining({ checked: false })); + expect(result[1]).toEqual(jasmine.objectContaining({ checked: true })); + }); + + it("returns the same array when toggling a disabled option", () => { + const items = [ + { value: "none", name: "None", checked: false, disabled: true }, + { value: "generic", name: "Generic", checked: true, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 0, ["none"]); + + expect(result).toBe(items); + expect(result[1]).toEqual(jasmine.objectContaining({ checked: true })); + }); + + it("preserves separators while clearing others for exclusive selection", () => { + const separator = new Separator("---"); + const items = [ + { value: "none", name: "None", checked: false, disabled: false }, + separator, + { value: "generic", name: "Generic", checked: true, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 0, ["none"]); + + expect(result[0]).toEqual(jasmine.objectContaining({ checked: true })); + expect(result[1]).toBe(separator); + expect(result[2]).toEqual(jasmine.objectContaining({ checked: false })); + }); + + it("does not clear others when turning an exclusive option off", () => { + const items = [ + { value: "none", name: "None", checked: true, disabled: false }, + { value: "generic", name: "Generic", checked: true, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 0, ["none"]); + + expect(result[0]).toEqual(jasmine.objectContaining({ checked: false })); + expect(result[1]).toEqual(jasmine.objectContaining({ checked: true })); + }); + + it("keeps disabled exclusive options unchanged when selecting another option", () => { + const items = [ + { value: "none", name: "None", checked: true, disabled: true }, + { value: "generic", name: "Generic", checked: false, disabled: false } + ]; + + const result = applyExclusiveToggle(items, 1, ["none"]); + + expect(result[0]).toEqual(jasmine.objectContaining({ checked: true })); + expect(result[1]).toEqual(jasmine.objectContaining({ checked: true })); + }); + + it("normalizes primitive choices to selectable items", () => { + const result = exclusiveCheckboxTesting.normalizeChoice("vscode") as any; + + expect(result).toEqual({ + value: "vscode", + name: "vscode", + checked: false, + disabled: false + }); + }); + + it("normalizes object choices and applies defaults", () => { + const result = exclusiveCheckboxTesting.normalizeChoice({ + value: "claude", + checked: true + }) as any; + + expect(result).toEqual({ + value: "claude", + name: "claude", + checked: true, + disabled: false + }); + }); + + it("detects separators as non-selectable and not checked", () => { + const separator = new Separator("---"); + + expect(exclusiveCheckboxTesting.isSelectable(separator as any)).toBe(false); + expect(exclusiveCheckboxTesting.isChecked(separator as any)).toBe(false); + }); + + it("moves to next selectable item when loop is disabled", () => { + const separator = new Separator("---"); + const items = [ + { value: "none", name: "None", checked: false, disabled: false }, + separator, + { value: "generic", name: "Generic", checked: false, disabled: true }, + { value: "claude", name: "Claude", checked: false, disabled: false } + ] as any; + + const result = exclusiveCheckboxTesting.moveActiveIndex(items, 0, 1, false); + + expect(result).toBe(3); + }); + + it("wraps to last selectable item when moving up with loop enabled", () => { + const items = [ + { value: "none", name: "None", checked: false, disabled: false }, + { value: "generic", name: "Generic", checked: false, disabled: true }, + { value: "claude", name: "Claude", checked: false, disabled: false } + ] as any; + + const result = exclusiveCheckboxTesting.moveActiveIndex(items, 0, -1, true); + + expect(result).toBe(2); + }); + + it("accepts numeric selection and submits selected value", async () => { + const { result, rendered } = await runExclusivePrompt(["2", "\r"], { + message: "Select agent", + choices: ["none", "claude"], + exclusiveValues: ["none"], + required: true, + loop: false + }); + + expect(result).toEqual(["claude"]); + expect(rendered).toContain("Select agent"); + }); + + it("shows required error then allows completion after selection", async () => { + const { result, rendered } = await runExclusivePrompt(["\r", " ", "\r"], { + message: "Select at least one", + choices: ["none", "claude"], + exclusiveValues: ["none"], + required: true, + loop: true + }); + + expect(result).toEqual(["none"]); + expect(rendered).toContain("Select at least one option."); + }); + + it("supports arrow navigation and space toggle", async () => { + const { result } = await runExclusivePrompt(["\u001b[B", " ", "\r"], { + message: "Select with arrows", + choices: ["none", "claude"], + exclusiveValues: ["none"], + required: true, + loop: false + }); + + expect(result).toEqual(["claude"]); + }); + }); + + describe("InquirerWrapper delegates", () => { + it("delegates input calls", async () => { + const context = {} as any; + const config = { message: "Input" } as any; + spyOn(inquirerWrapperTesting.promptDelegates, "input").and.returnValue(Promise.resolve("value") as any); + + const result = await LocalInquirerWrapper.input(config, context); + + expect(result).toBe("value"); + expect(inquirerWrapperTesting.promptDelegates.input).toHaveBeenCalledWith(config, context); + }); + + it("delegates select calls", async () => { + const context = {} as any; + const config = { message: "Select", choices: ["A"] } as any; + spyOn(inquirerWrapperTesting.promptDelegates, "select").and.returnValue(Promise.resolve("A") as any); + + const result = await LocalInquirerWrapper.select(config, context); + + expect(result).toBe("A"); + expect(inquirerWrapperTesting.promptDelegates.select).toHaveBeenCalledWith(config, context); + }); + + it("delegates checkbox calls", async () => { + const context = {} as any; + const config = { message: "Check", choices: ["A"] } as any; + spyOn(inquirerWrapperTesting.promptDelegates, "checkbox").and.returnValue(Promise.resolve(["A"]) as any); + + const result = await LocalInquirerWrapper.checkbox(config, context); + + expect(result).toEqual(["A"]); + expect(inquirerWrapperTesting.promptDelegates.checkbox).toHaveBeenCalledWith(config, context); + }); + + it("delegates exclusive checkbox calls", async () => { + const context = {} as any; + const config = { message: "Exclusive", choices: [{ value: "none" }], exclusiveValues: ["none"] } as any; + spyOn(inquirerWrapperTesting.promptDelegates, "exclusiveCheckbox").and.returnValue(Promise.resolve(["none"]) as any); + + const result = await LocalInquirerWrapper.exclusiveCheckbox(config, context); + + expect(result).toEqual(["none"]); + expect(inquirerWrapperTesting.promptDelegates.exclusiveCheckbox).toHaveBeenCalledWith(config, context); + }); + + it("delegates confirm calls", async () => { + const context = {} as any; + const config = { message: "Confirm", default: true }; + spyOn(inquirerWrapperTesting.promptDelegates, "confirm").and.returnValue(Promise.resolve(true) as any); + + const result = await LocalInquirerWrapper.confirm(config, context); + + expect(result).toBe(true); + expect(inquirerWrapperTesting.promptDelegates.confirm).toHaveBeenCalledWith(config, context); + }); + }); + describe("configureSkills", () => { const angularSkillsDir = "node_modules/igniteui-angular/skills"; @@ -604,14 +859,14 @@ describe("Unit - ai-config command", () => { it("prompts for agents when --agent is not provided", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValues( + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValues( Promise.resolve(["claude"]), Promise.resolve(["vscode"]) ); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "angular" }); - expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ + expect(InquirerWrapper.exclusiveCheckbox).toHaveBeenCalledWith(jasmine.objectContaining({ message: "Which AI agents do you want to generate skills and instructions for?", required: true })); @@ -622,18 +877,18 @@ describe("Unit - ai-config command", () => { it("uses defaults without prompting when canPrompt returns false", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(false); - spyOn(InquirerWrapper, "checkbox"); + spyOn(InquirerWrapper, "exclusiveCheckbox"); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "angular" }); - expect(InquirerWrapper.checkbox).not.toHaveBeenCalled(); + expect(InquirerWrapper.exclusiveCheckbox).not.toHaveBeenCalled(); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: generic, claude; assistant: generic", cd1: "angular" })); }); it("logs skipping and does not post analytics when none is selected", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"])); + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValue(Promise.resolve(["none"])); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "angular" }); @@ -646,7 +901,7 @@ describe("Unit - ai-config command", () => { const mockFs = createMockFs(); App.container.set(FS_TOKEN, mockFs); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValues( + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValues( Promise.resolve(["none"]), Promise.resolve(["vscode"]) ); @@ -658,7 +913,7 @@ describe("Unit - ai-config command", () => { expect(config.servers).toBeDefined(); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "Ai Config" })); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: none; assistant: vscode", cd1: "angular" })); - expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(2); + expect(InquirerWrapper.exclusiveCheckbox).toHaveBeenCalledTimes(2); expect( (Util.log as jasmine.Spy).calls.allArgs() .filter(([msg]) => String(msg).includes("Skipping")) @@ -669,14 +924,14 @@ describe("Unit - ai-config command", () => { it("configures multiple agents when selected interactively", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValues( + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValues( Promise.resolve(["claude", "cursor"]), Promise.resolve(["vscode"]) ); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "angular" }); - expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ + expect(InquirerWrapper.exclusiveCheckbox).toHaveBeenCalledWith(jasmine.objectContaining({ message: "Which AI agents do you want to generate skills and instructions for?" })); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: claude, cursor; assistant: vscode", cd1: "angular" })); @@ -685,11 +940,11 @@ describe("Unit - ai-config command", () => { it("skips prompt when --agent is provided", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["vscode"])); + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValue(Promise.resolve(["vscode"])); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", agents: ["cursor"], framework: "angular" }); - expect(InquirerWrapper.checkbox).not.toHaveBeenCalledWith(jasmine.objectContaining({ + expect(InquirerWrapper.exclusiveCheckbox).not.toHaveBeenCalledWith(jasmine.objectContaining({ message: "Which AI agents do you want to generate skills and instructions for?" })); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: cursor; assistant: vscode", cd1: "angular" })); @@ -698,24 +953,24 @@ describe("Unit - ai-config command", () => { it("skips assistant prompt when --assistant is provided", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"])); + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValue(Promise.resolve(["claude"])); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", assistants: ["cursor"], framework: "angular" }); - expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(1); + expect(InquirerWrapper.exclusiveCheckbox).toHaveBeenCalledTimes(1); }); it("prompts for assistant with correct message", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValues( + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValues( Promise.resolve(["claude"]), Promise.resolve(["vscode"]) ); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "angular" }); - expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ + expect(InquirerWrapper.exclusiveCheckbox).toHaveBeenCalledWith(jasmine.objectContaining({ message: "Which coding assistants should MCP servers be configured for?" })); }); @@ -724,7 +979,7 @@ describe("Unit - ai-config command", () => { const mockFs = createMockFs(); App.container.set(FS_TOKEN, mockFs); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValues( + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValues( Promise.resolve(["claude"]), Promise.resolve(["generic"]) ); @@ -739,12 +994,12 @@ describe("Unit - ai-config command", () => { it("logs and returns early when framework is jquery", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox"); + spyOn(InquirerWrapper, "exclusiveCheckbox"); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", framework: "jquery" }); expect(Util.log).toHaveBeenCalledWith("AI Config currently not available for jQuery projects."); - expect(InquirerWrapper.checkbox).not.toHaveBeenCalled(); + expect(InquirerWrapper.exclusiveCheckbox).not.toHaveBeenCalled(); }); describe("framework resolution", () => { @@ -752,7 +1007,7 @@ describe("Unit - ai-config command", () => { beforeEach(() => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"])); + spyOn(InquirerWrapper, "exclusiveCheckbox").and.returnValue(Promise.resolve(["none"])); }); it("uses detected framework when --framework is not provided", async () => {