Skip to content

Commit 2c7b425

Browse files
committed
feat: improve CLI initialization and add new agents
- Add language and Primary Agent selection to CLI init command, add data-engineer/mobile-developer/i18n-specialist agents, and improve agent resolution logic close #151
1 parent 7774b47 commit 2c7b425

18 files changed

Lines changed: 2733 additions & 29 deletions

apps/mcp-server/src/cli/init/init.command.spec.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const {
1313
mockRenderConfigAsJs,
1414
mockRenderConfigAsJson,
1515
mockPromptModelSelection,
16+
mockPromptLanguageSelection,
17+
mockPromptPrimaryAgentSelection,
1618
} = vi.hoisted(() => ({
1719
mockAnalyzeProject: vi.fn(),
1820
mockGenerate: vi.fn(),
@@ -22,6 +24,8 @@ const {
2224
mockRenderConfigAsJs: vi.fn(),
2325
mockRenderConfigAsJson: vi.fn(),
2426
mockPromptModelSelection: vi.fn(),
27+
mockPromptLanguageSelection: vi.fn(),
28+
mockPromptPrimaryAgentSelection: vi.fn(),
2529
}));
2630

2731
// Mock all modules
@@ -50,7 +54,11 @@ vi.mock('./templates', () => ({
5054

5155
vi.mock('./prompts', () => ({
5256
promptModelSelection: mockPromptModelSelection,
57+
promptLanguageSelection: mockPromptLanguageSelection,
58+
promptPrimaryAgentSelection: mockPromptPrimaryAgentSelection,
5359
DEFAULT_MODEL_CHOICE: 'claude-sonnet-4-20250514',
60+
DEFAULT_LANGUAGE: 'ko',
61+
DEFAULT_PRIMARY_AGENT: 'frontend-developer',
5462
}));
5563

5664
vi.mock('../utils/console', () => ({
@@ -130,6 +138,8 @@ describe('init.command', () => {
130138
mockRenderConfigAsJs.mockReturnValue('// rendered config');
131139
mockRenderConfigAsJson.mockReturnValue('{}');
132140
mockPromptModelSelection.mockResolvedValue('claude-sonnet-4-20250514');
141+
mockPromptLanguageSelection.mockResolvedValue('ko');
142+
mockPromptPrimaryAgentSelection.mockResolvedValue('frontend-developer');
133143
});
134144

135145
describe('getApiKey', () => {
@@ -246,12 +256,13 @@ describe('init.command', () => {
246256
);
247257
});
248258

249-
it('should pass language option to renderer', async () => {
259+
it('should pass language option to renderer when skipPrompts', async () => {
250260
const options: InitOptions = {
251261
projectRoot: '/project',
252262
format: 'js',
253263
force: false,
254264
language: 'en',
265+
skipPrompts: true,
255266
};
256267

257268
await runInit(options);
@@ -262,7 +273,7 @@ describe('init.command', () => {
262273
);
263274
});
264275

265-
it('should call promptModelSelection when skipPrompts is false', async () => {
276+
it('should call all prompts when skipPrompts is false', async () => {
266277
const options: InitOptions = {
267278
projectRoot: '/project',
268279
format: 'js',
@@ -272,10 +283,12 @@ describe('init.command', () => {
272283

273284
await runInit(options);
274285

286+
expect(mockPromptLanguageSelection).toHaveBeenCalled();
287+
expect(mockPromptPrimaryAgentSelection).toHaveBeenCalled();
275288
expect(mockPromptModelSelection).toHaveBeenCalled();
276289
});
277290

278-
it('should skip promptModelSelection when skipPrompts is true', async () => {
291+
it('should skip all prompts when skipPrompts is true', async () => {
279292
const options: InitOptions = {
280293
projectRoot: '/project',
281294
format: 'js',
@@ -285,10 +298,14 @@ describe('init.command', () => {
285298

286299
await runInit(options);
287300

301+
expect(mockPromptLanguageSelection).not.toHaveBeenCalled();
302+
expect(mockPromptPrimaryAgentSelection).not.toHaveBeenCalled();
288303
expect(mockPromptModelSelection).not.toHaveBeenCalled();
289304
});
290305

291-
it('should pass selected model to renderer', async () => {
306+
it('should pass selected values to renderer', async () => {
307+
mockPromptLanguageSelection.mockResolvedValue('ja');
308+
mockPromptPrimaryAgentSelection.mockResolvedValue('backend-developer');
292309
mockPromptModelSelection.mockResolvedValue('claude-opus-4-20250514');
293310

294311
const options: InitOptions = {
@@ -302,11 +319,15 @@ describe('init.command', () => {
302319

303320
expect(mockRenderConfigAsJs).toHaveBeenCalledWith(
304321
mockTemplateResult.template,
305-
expect.objectContaining({ defaultModel: 'claude-opus-4-20250514' }),
322+
expect.objectContaining({
323+
language: 'ja',
324+
primaryAgent: 'backend-developer',
325+
defaultModel: 'claude-opus-4-20250514',
326+
}),
306327
);
307328
});
308329

309-
it('should use default model when skipPrompts is true', async () => {
330+
it('should use default values when skipPrompts is true', async () => {
310331
const options: InitOptions = {
311332
projectRoot: '/project',
312333
format: 'js',
@@ -318,7 +339,11 @@ describe('init.command', () => {
318339

319340
expect(mockRenderConfigAsJs).toHaveBeenCalledWith(
320341
mockTemplateResult.template,
321-
expect.objectContaining({ defaultModel: 'claude-sonnet-4-20250514' }),
342+
expect.objectContaining({
343+
language: 'ko',
344+
primaryAgent: 'frontend-developer',
345+
defaultModel: 'claude-sonnet-4-20250514',
346+
}),
322347
);
323348
});
324349
});

apps/mcp-server/src/cli/init/init.command.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import {
1515
renderConfigAsJs,
1616
renderConfigAsJson,
1717
} from './templates';
18-
import { promptModelSelection, DEFAULT_MODEL_CHOICE } from './prompts';
18+
import {
19+
promptModelSelection,
20+
promptLanguageSelection,
21+
promptPrimaryAgentSelection,
22+
DEFAULT_MODEL_CHOICE,
23+
DEFAULT_LANGUAGE,
24+
DEFAULT_PRIMARY_AGENT,
25+
} from './prompts';
1926
import type { InitOptions, InitResult } from '../cli.types';
2027

2128
/**
@@ -69,10 +76,24 @@ async function runTemplateInit(
6976
console.log.info(` Detected: ${detectedFrameworks.join(', ')}`);
7077
}
7178

72-
// Step 3: Select AI model
79+
// Step 3: Interactive prompts
80+
let selectedLanguage = options.language ?? DEFAULT_LANGUAGE;
7381
let selectedModel = DEFAULT_MODEL_CHOICE;
82+
let selectedAgent = DEFAULT_PRIMARY_AGENT;
83+
7484
const shouldPrompt = !(options.skipPrompts ?? false);
7585
if (shouldPrompt) {
86+
// 3a: Language selection
87+
console.log.step('🌐', 'Select response language...');
88+
selectedLanguage = await promptLanguageSelection();
89+
console.log.success(`Language: ${selectedLanguage}`);
90+
91+
// 3b: Primary agent selection
92+
console.log.step('👤', 'Select primary development agent...');
93+
selectedAgent = await promptPrimaryAgentSelection();
94+
console.log.success(`Agent: ${selectedAgent}`);
95+
96+
// 3c: AI model selection
7697
console.log.step('🤖', 'Select AI model...');
7798
selectedModel = await promptModelSelection();
7899
console.log.success(`Model: ${selectedModel}`);
@@ -84,8 +105,9 @@ async function runTemplateInit(
84105
const projectName = analysis.packageInfo?.name;
85106
const renderOptions = {
86107
projectName,
87-
language: options.language,
108+
language: selectedLanguage,
88109
defaultModel: selectedModel,
110+
primaryAgent: selectedAgent,
89111
};
90112

91113
const configContent =
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getPrimaryAgentChoices, DEFAULT_PRIMARY_AGENT } from './agent-prompt';
3+
import { ACT_PRIMARY_AGENTS } from '../../../keyword/keyword.types';
4+
5+
describe('agent-prompt', () => {
6+
describe('DEFAULT_PRIMARY_AGENT', () => {
7+
it('should be frontend-developer', () => {
8+
expect(DEFAULT_PRIMARY_AGENT).toBe('frontend-developer');
9+
});
10+
});
11+
12+
describe('getPrimaryAgentChoices', () => {
13+
it('should return an array of agent choices', () => {
14+
const choices = getPrimaryAgentChoices();
15+
expect(Array.isArray(choices)).toBe(true);
16+
expect(choices.length).toBeGreaterThan(0);
17+
});
18+
19+
it('should include all ACT primary agents', () => {
20+
const choices = getPrimaryAgentChoices();
21+
const values = choices.map(c => c.value);
22+
23+
for (const agent of ACT_PRIMARY_AGENTS) {
24+
expect(values).toContain(agent);
25+
}
26+
});
27+
28+
it('should have name and value for each choice', () => {
29+
const choices = getPrimaryAgentChoices();
30+
for (const choice of choices) {
31+
expect(choice.name).toBeDefined();
32+
expect(choice.value).toBeDefined();
33+
expect(typeof choice.name).toBe('string');
34+
expect(typeof choice.value).toBe('string');
35+
}
36+
});
37+
38+
it('should include frontend-developer with recommended label', () => {
39+
const choices = getPrimaryAgentChoices();
40+
const frontend = choices.find(c => c.value === 'frontend-developer');
41+
expect(frontend).toBeDefined();
42+
expect(frontend?.name).toContain('Recommended');
43+
});
44+
45+
it('should include descriptions for agents', () => {
46+
const choices = getPrimaryAgentChoices();
47+
const withDescriptions = choices.filter(c => c.description);
48+
expect(withDescriptions.length).toBeGreaterThan(0);
49+
});
50+
51+
it('should include new primary agents (data-engineer, mobile-developer)', () => {
52+
const choices = getPrimaryAgentChoices();
53+
const values = choices.map(c => c.value);
54+
55+
expect(values).toContain('data-engineer');
56+
expect(values).toContain('mobile-developer');
57+
});
58+
});
59+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Primary Agent Selection Prompt
3+
*
4+
* Interactive CLI prompt for primary development agent selection
5+
*/
6+
7+
import { select } from '@inquirer/prompts';
8+
import {
9+
ACT_PRIMARY_AGENTS,
10+
DEFAULT_ACT_AGENT,
11+
ACT_AGENT_DISPLAY_INFO,
12+
type ActPrimaryAgent,
13+
} from '../../../keyword/keyword.types';
14+
15+
/**
16+
* Agent choice option for the CLI prompt
17+
*/
18+
export interface AgentChoice {
19+
name: string;
20+
value: string;
21+
description?: string;
22+
}
23+
24+
/**
25+
* Default primary agent - re-exported from keyword.types for backward compatibility
26+
*/
27+
export const DEFAULT_PRIMARY_AGENT = DEFAULT_ACT_AGENT;
28+
29+
/**
30+
* Get available primary agent choices for the CLI prompt
31+
*/
32+
export function getPrimaryAgentChoices(): AgentChoice[] {
33+
return ACT_PRIMARY_AGENTS.map(agentId => {
34+
const info = ACT_AGENT_DISPLAY_INFO[agentId as ActPrimaryAgent];
35+
const isDefault = agentId === DEFAULT_PRIMARY_AGENT;
36+
return {
37+
name: isDefault ? `${info.name} (Recommended)` : info.name,
38+
value: agentId,
39+
description: info.description,
40+
};
41+
});
42+
}
43+
44+
/**
45+
* Prompt user to select primary development agent
46+
* @param message - Custom message for the prompt
47+
* @returns Selected agent ID
48+
*/
49+
export async function promptPrimaryAgentSelection(
50+
message = 'Select your primary development agent:',
51+
): Promise<string> {
52+
const choices = getPrimaryAgentChoices();
53+
54+
return select({
55+
message,
56+
choices,
57+
default: DEFAULT_PRIMARY_AGENT,
58+
});
59+
}

apps/mcp-server/src/cli/init/prompts/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,19 @@ export {
1111
} from './model-prompt';
1212

1313
export type { ModelChoice } from './model-prompt';
14+
15+
export {
16+
getLanguageChoices,
17+
promptLanguageSelection,
18+
DEFAULT_LANGUAGE,
19+
} from './language-prompt';
20+
21+
export type { LanguageChoice } from './language-prompt';
22+
23+
export {
24+
getPrimaryAgentChoices,
25+
promptPrimaryAgentSelection,
26+
DEFAULT_PRIMARY_AGENT,
27+
} from './agent-prompt';
28+
29+
export type { AgentChoice } from './agent-prompt';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getLanguageChoices, DEFAULT_LANGUAGE } from './language-prompt';
3+
4+
describe('language-prompt', () => {
5+
describe('DEFAULT_LANGUAGE', () => {
6+
it('should be Korean (ko)', () => {
7+
expect(DEFAULT_LANGUAGE).toBe('ko');
8+
});
9+
});
10+
11+
describe('getLanguageChoices', () => {
12+
it('should return an array of language choices', () => {
13+
const choices = getLanguageChoices();
14+
expect(Array.isArray(choices)).toBe(true);
15+
expect(choices.length).toBeGreaterThan(0);
16+
});
17+
18+
it('should include Korean as first option', () => {
19+
const choices = getLanguageChoices();
20+
expect(choices[0].value).toBe('ko');
21+
expect(choices[0].name).toContain('Korean');
22+
});
23+
24+
it('should include English option', () => {
25+
const choices = getLanguageChoices();
26+
const english = choices.find(c => c.value === 'en');
27+
expect(english).toBeDefined();
28+
expect(english?.name).toBe('English');
29+
});
30+
31+
it('should have name, value, and description for each choice', () => {
32+
const choices = getLanguageChoices();
33+
for (const choice of choices) {
34+
expect(choice.name).toBeDefined();
35+
expect(choice.value).toBeDefined();
36+
expect(typeof choice.name).toBe('string');
37+
expect(typeof choice.value).toBe('string');
38+
}
39+
});
40+
41+
it('should include all supported languages', () => {
42+
const choices = getLanguageChoices();
43+
const values = choices.map(c => c.value);
44+
expect(values).toContain('ko');
45+
expect(values).toContain('en');
46+
expect(values).toContain('ja');
47+
expect(values).toContain('zh');
48+
expect(values).toContain('es');
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)