Skip to content
Open
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
18 changes: 18 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,24 @@ export const SETTINGS_SCHEMA = {
description: 'The API key for the Tavily API.',
showInDialog: false,
},
coreCommands: {
type: 'array',
label: 'Core Commands',
category: 'Commands',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'A list of core command names to load. If specified, only these commands will be loaded.',
showInDialog: false,
},
excludeCommands: {
type: 'array',
label: 'Exclude Commands',
category: 'Commands',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'A list of command names to exclude from loading.',
showInDialog: false,
},
skipNextSpeakerCheck: {
type: 'boolean',
label: 'Skip Next Speaker Check',
Expand Down
237 changes: 236 additions & 1 deletion packages/cli/src/services/CommandService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -64,6 +65,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[loader1, loader2],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -85,6 +87,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[loader1, loader2],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand Down Expand Up @@ -114,6 +117,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[loader1, emptyLoader, loader3],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -134,6 +138,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[successfulLoader, failingLoader],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -149,6 +154,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[new MockCommandLoader([mockCommandA])],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -170,7 +176,7 @@ describe('CommandService', () => {
const loader1 = new MockCommandLoader([mockCommandA]);
const loader2 = new MockCommandLoader([mockCommandB]);

await CommandService.create([loader1, loader2], signal);
await CommandService.create([loader1, loader2], signal, undefined);

expect(loader1.loadCommands).toHaveBeenCalledTimes(1);
expect(loader1.loadCommands).toHaveBeenCalledWith(signal);
Expand Down Expand Up @@ -202,6 +208,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[mockLoader1, mockLoader2],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand Down Expand Up @@ -252,6 +259,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[mockLoader1, mockLoader2],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand Down Expand Up @@ -289,6 +297,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand Down Expand Up @@ -337,6 +346,7 @@ describe('CommandService', () => {
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
undefined,
);

const commands = service.getCommands();
Expand All @@ -349,4 +359,229 @@ describe('CommandService', () => {
expect(deployExtension).toBeDefined();
expect(deployExtension?.description).toBe('[gcp] Deploy to Google Cloud');
});

it('should filter commands based on coreCommands setting', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);
const commandC = createMockCommand('command-c', CommandKind.FILE);

const mockLoader = new MockCommandLoader([commandA, commandB, commandC]);

const settings = {
coreCommands: ['command-a', 'command-c'],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(2);
expect(commands).toEqual(
expect.arrayContaining([commandA, commandC]),
);
expect(commands).not.toContain(commandB);
});

it('should filter commands based on excludeCommands setting', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);
const commandC = createMockCommand('command-c', CommandKind.FILE);

const mockLoader = new MockCommandLoader([commandA, commandB, commandC]);

const settings = {
excludeCommands: ['command-b'],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(2);
expect(commands).toEqual(
expect.arrayContaining([commandA, commandC]),
);
expect(commands).not.toContain(commandB);
});

it('should exclude commands that appear in both coreCommands and excludeCommands', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);
const commandC = createMockCommand('command-c', CommandKind.FILE);

const mockLoader = new MockCommandLoader([commandA, commandB, commandC]);

const settings = {
coreCommands: ['command-a', 'command-b'],
excludeCommands: ['command-b', 'command-c'],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual(
expect.arrayContaining([commandA]),
);
expect(commands).not.toContain(commandB);
expect(commands).not.toContain(commandC);
});

it('should handle empty coreCommands array', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: [],
excludeCommands: ['command-b'],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual(
expect.arrayContaining([commandA]),
);
expect(commands).not.toContain(commandB);
});

it('should handle empty excludeCommands array', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: ['command-a'],
excludeCommands: [],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual(
expect.arrayContaining([commandA]),
);
expect(commands).not.toContain(commandB);
});

it('should handle both arrays being empty', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: [],
excludeCommands: [],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(2);
expect(commands).toEqual(
expect.arrayContaining([commandA, commandB]),
);
});

it('should handle empty coreCommands array', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: [],
excludeCommands: ['command-b'],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual(
expect.arrayContaining([commandA]),
);
expect(commands).not.toContain(commandB);
});

it('should handle empty excludeCommands array', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: ['command-a'],
excludeCommands: [],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands).toEqual(
expect.arrayContaining([commandA]),
);
expect(commands).not.toContain(commandB);
});

it('should handle both arrays being empty', async () => {
const commandA = createMockCommand('command-a', CommandKind.BUILT_IN);
const commandB = createMockCommand('command-b', CommandKind.BUILT_IN);

const mockLoader = new MockCommandLoader([commandA, commandB]);

const settings = {
coreCommands: [],
excludeCommands: [],
};

const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
settings,
);

const commands = service.getCommands();
expect(commands).toHaveLength(2);
expect(commands).toEqual(
expect.arrayContaining([commandA, commandB]),
);
});
});
27 changes: 24 additions & 3 deletions packages/cli/src/services/CommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { SlashCommand } from '../ui/commands/types.js';
import type { ICommandLoader } from './types.js';
import { SlashCommand } from '../ui/commands/types.js';
import { ICommandLoader } from './types.js';
import { Settings } from '../config/settings.js';

/**
* Orchestrates the discovery and loading of all slash commands for the CLI.
Expand Down Expand Up @@ -42,11 +43,13 @@ export class CommandService {
* @param loaders An array of objects that conform to the `ICommandLoader`
* interface. Built-in commands should come first, followed by FileCommandLoader.
* @param signal An AbortSignal to cancel the loading process.
* @param settings Optional settings to control which commands to load.
* @returns A promise that resolves to a new, fully initialized `CommandService` instance.
*/
static async create(
loaders: ICommandLoader[],
signal: AbortSignal,
settings?: Settings,
): Promise<CommandService> {
const results = await Promise.allSettled(
loaders.map((loader) => loader.loadCommands(signal)),
Expand All @@ -61,8 +64,26 @@ export class CommandService {
}
}

// Filter commands based on coreCommands and excludeCommands settings
let filteredCommands = allCommands;

// Create sets for efficient lookup
const coreCommandSet = new Set(settings?.coreCommands || []);
const excludeCommandSet = new Set(settings?.excludeCommands || []);

// If coreCommands is specified, only include those commands
if (coreCommandSet.size > 0) {
filteredCommands = filteredCommands.filter(cmd =>
coreCommandSet.has(cmd.name) && !excludeCommandSet.has(cmd.name)
);
} else if (excludeCommandSet.size > 0) {
filteredCommands = filteredCommands.filter(cmd =>
!excludeCommandSet.has(cmd.name)
);
}

const commandMap = new Map<string, SlashCommand>();
for (const cmd of allCommands) {
for (const cmd of filteredCommands) {
let finalName = cmd.name;

// Extension commands get renamed if they conflict with existing commands
Expand Down
Loading
Loading