Skip to content

Commit bac0da5

Browse files
committed
Refactor command with terminal ui
1 parent fe3c8f0 commit bac0da5

15 files changed

Lines changed: 467 additions & 141 deletions

File tree

.ai-devkit.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
22
"version": "0.4.2",
33
"environments": [
4-
"cursor",
5-
"claude"
4+
"cursor"
65
],
76
"initializedPhases": [
87
"requirements",
@@ -12,5 +11,5 @@
1211
"testing"
1312
],
1413
"createdAt": "2025-12-28T13:35:45.251Z",
15-
"updatedAt": "2026-01-26T20:02:55.191Z"
14+
"updatedAt": "2026-01-28T20:00:27.088Z"
1615
}

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"author": "",
2828
"license": "MIT",
2929
"dependencies": {
30-
"@ai-devkit/memory": "^0.2.0",
30+
"@ai-devkit/memory": "*",
3131
"chalk": "^4.1.2",
3232
"commander": "^11.1.0",
3333
"fs-extra": "^11.2.0",
@@ -52,4 +52,4 @@
5252
"engines": {
5353
"node": ">=16.0.0"
5454
}
55-
}
55+
}

packages/cli/src/__tests__/lib/SkillManager.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ jest.mock("../../lib/EnvironmentSelector");
2626
jest.mock("../../lib/GlobalConfig");
2727
jest.mock("../../util/git");
2828
jest.mock("../../util/skill");
29+
jest.mock("ora", () => {
30+
return jest.fn(() => ({
31+
start: jest.fn().mockReturnThis(),
32+
succeed: jest.fn().mockReturnThis(),
33+
fail: jest.fn().mockReturnThis(),
34+
warn: jest.fn().mockReturnThis(),
35+
stop: jest.fn().mockReturnThis(),
36+
text: '',
37+
isSpinning: false,
38+
}));
39+
});
2940

3041
const mockedFs = fs as jest.Mocked<typeof fs>;
3142
const mockedHttps = https as jest.Mocked<typeof https>;

packages/cli/src/__tests__/util/terminal-ui.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ jest.mock('chalk', () => ({
66
yellow: (text: string) => `[YELLOW]${text}[/YELLOW]`,
77
red: (text: string) => `[RED]${text}[/RED]`,
88
cyan: (text: string) => `[CYAN]${text}[/CYAN]`,
9+
dim: (text: string) => `[DIM]${text}[/DIM]`,
10+
bold: (text: string) => `[BOLD]${text}[/BOLD]`,
911
}));
1012

1113
const mockOraInstance = {
@@ -173,6 +175,185 @@ describe('TerminalUI', () => {
173175
});
174176
});
175177

178+
describe('table()', () => {
179+
beforeEach(() => {
180+
// Need to mock chalk for table tests
181+
jest.mock('chalk', () => ({
182+
blue: (text: string) => `[BLUE]${text}[/BLUE]`,
183+
green: (text: string) => `[GREEN]${text}[/GREEN]`,
184+
yellow: (text: string) => `[YELLOW]${text}[/YELLOW]`,
185+
red: (text: string) => `[RED]${text}[/RED]`,
186+
cyan: (text: string) => `[CYAN]${text}[/CYAN]`,
187+
dim: (text: string) => `[DIM]${text}[/DIM]`,
188+
bold: (text: string) => `[BOLD]${text}[/BOLD]`,
189+
}));
190+
});
191+
192+
it('should display table with headers and rows', () => {
193+
ui.table({
194+
headers: ['Name', 'Status'],
195+
rows: [
196+
['skill-1', 'active'],
197+
['skill-2', 'inactive']
198+
]
199+
});
200+
201+
expect(consoleLogSpy).toHaveBeenCalled();
202+
// Should have header, separator, and 2 rows = 4 calls
203+
expect(consoleLogSpy).toHaveBeenCalledTimes(4);
204+
});
205+
206+
it('should apply column styles when provided', () => {
207+
const chalk = require('chalk');
208+
209+
ui.table({
210+
headers: ['Name', 'Type'],
211+
rows: [['test', 'demo']],
212+
columnStyles: [chalk.cyan, chalk.green]
213+
});
214+
215+
expect(consoleLogSpy).toHaveBeenCalled();
216+
});
217+
218+
it('should use default indent of 2 spaces', () => {
219+
ui.table({
220+
headers: ['Col1'],
221+
rows: [['data']]
222+
});
223+
224+
const calls = consoleLogSpy.mock.calls;
225+
// Check that rows start with indent
226+
expect(calls[2][0]).toMatch(/^ /);
227+
});
228+
229+
it('should use custom indent when provided', () => {
230+
ui.table({
231+
headers: ['Col1'],
232+
rows: [['data']],
233+
indent: ' '
234+
});
235+
236+
const calls = consoleLogSpy.mock.calls;
237+
// Check that rows start with custom indent
238+
expect(calls[2][0]).toMatch(/^ /);
239+
});
240+
241+
it('should handle empty rows', () => {
242+
ui.table({
243+
headers: ['Name', 'Status'],
244+
rows: []
245+
});
246+
247+
// Should still display headers and separator
248+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
249+
});
250+
251+
it('should pad columns correctly', () => {
252+
ui.table({
253+
headers: ['Short', 'VeryLongHeader'],
254+
rows: [
255+
['a', 'b'],
256+
['longer', 'c']
257+
]
258+
});
259+
260+
expect(consoleLogSpy).toHaveBeenCalled();
261+
// Verify padding is applied (all calls should have content)
262+
consoleLogSpy.mock.calls.forEach((call: any[]) => {
263+
expect(call[0]).toBeTruthy();
264+
});
265+
});
266+
});
267+
268+
describe('summary()', () => {
269+
it('should display summary with title and items', () => {
270+
ui.summary({
271+
items: [
272+
{ type: 'success', count: 5, label: 'updated' },
273+
{ type: 'warning', count: 2, label: 'skipped' },
274+
{ type: 'error', count: 1, label: 'failed' }
275+
]
276+
});
277+
278+
expect(consoleLogSpy).toHaveBeenCalled();
279+
// Should have title + 3 items = 4 calls
280+
expect(consoleLogSpy).toHaveBeenCalledTimes(4);
281+
});
282+
283+
it('should use custom title when provided', () => {
284+
ui.summary({
285+
title: 'Custom Summary',
286+
items: [
287+
{ type: 'success', count: 1, label: 'done' }
288+
]
289+
});
290+
291+
const calls = consoleLogSpy.mock.calls;
292+
expect(calls[0][0]).toContain('Custom Summary');
293+
});
294+
295+
it('should skip items with count 0', () => {
296+
ui.summary({
297+
items: [
298+
{ type: 'success', count: 0, label: 'updated' },
299+
{ type: 'error', count: 1, label: 'failed' }
300+
]
301+
});
302+
303+
// Should only display title + 1 item (skipping the 0 count)
304+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
305+
});
306+
307+
it('should display details section when provided', () => {
308+
ui.summary({
309+
items: [
310+
{ type: 'error', count: 1, label: 'failed' }
311+
],
312+
details: {
313+
title: 'Errors',
314+
items: [
315+
{ message: 'Error 1', tip: 'Fix this' },
316+
{ message: 'Error 2' }
317+
]
318+
}
319+
});
320+
321+
expect(consoleLogSpy).toHaveBeenCalled();
322+
// Title + 1 item + details title + 2 errors (with 1 tip) = 6 calls
323+
expect(consoleLogSpy).toHaveBeenCalledTimes(6);
324+
});
325+
326+
it('should not display details section when items are empty', () => {
327+
ui.summary({
328+
items: [
329+
{ type: 'success', count: 1, label: 'done' }
330+
],
331+
details: {
332+
title: 'Errors',
333+
items: []
334+
}
335+
});
336+
337+
// Should only display summary, not details
338+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
339+
});
340+
341+
it('should apply correct colors for different types', () => {
342+
ui.summary({
343+
items: [
344+
{ type: 'success', count: 1, label: 'ok' },
345+
{ type: 'warning', count: 1, label: 'warn' },
346+
{ type: 'error', count: 1, label: 'err' },
347+
{ type: 'info', count: 1, label: 'info' }
348+
]
349+
});
350+
351+
expect(consoleLogSpy).toHaveBeenCalled();
352+
// Verify all types were displayed
353+
expect(consoleLogSpy).toHaveBeenCalledTimes(5); // title + 4 items
354+
});
355+
});
356+
176357
describe('Edge cases', () => {
177358
it('should handle very long messages', () => {
178359
const longMessage = 'a'.repeat(1000);

packages/cli/src/commands/init.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { execSync } from 'child_process';
22
import inquirer from 'inquirer';
3-
import chalk from 'chalk';
43
import { ConfigManager } from '../lib/Config';
54
import { TemplateManager } from '../lib/TemplateManager';
65
import { EnvironmentSelector } from '../lib/EnvironmentSelector';
@@ -72,7 +71,7 @@ export async function initCommand(options: InitOptions) {
7271

7372
let selectedEnvironments: EnvironmentCode[] = options.environment || [];
7473
if (selectedEnvironments.length === 0) {
75-
ui.info('\nAI Environment Setup\n');
74+
ui.info('AI Environment Setup');
7675
selectedEnvironments = await environmentSelector.selectEnvironments();
7776
}
7877

@@ -112,7 +111,7 @@ export async function initCommand(options: InitOptions) {
112111
return;
113112
}
114113

115-
ui.info('\nInitializing AI DevKit...\n');
114+
ui.text('Initializing AI DevKit...', { breakline: true });
116115

117116
let config = await configManager.read();
118117
if (!config) {
@@ -126,7 +125,7 @@ export async function initCommand(options: InitOptions) {
126125
environmentSelector.displaySelectionSummary(selectedEnvironments);
127126

128127
phaseSelector.displaySelectionSummary(selectedPhases);
129-
ui.info('\nSetting up environment templates...\n');
128+
ui.text('Setting up environment templates...', { breakline: true });
130129
const envFiles = await templateManager.setupMultipleEnvironments(selectedEnvironments);
131130
envFiles.forEach(file => {
132131
ui.success(`Created ${file}`);
@@ -157,11 +156,11 @@ export async function initCommand(options: InitOptions) {
157156
}
158157
}
159158

160-
ui.success('\nAI DevKit initialized successfully!\n');
159+
ui.text('AI DevKit initialized successfully!', { breakline: true });
161160
ui.info('Next steps:');
162-
ui.info(' • Review and customize templates in docs/ai/');
163-
ui.info(' • Your AI environments are ready to use with the generated configurations');
164-
ui.info(' • Run `ai-devkit phase <name>` to add more phases later');
165-
ui.info(' • Run `ai-devkit init` again to add more environments\n');
161+
ui.text(' • Review and customize templates in docs/ai/');
162+
ui.text(' • Your AI environments are ready to use with the generated configurations');
163+
ui.text(' • Run `ai-devkit phase <name>` to add more phases later');
164+
ui.text(' • Run `ai-devkit init` again to add more environments\n');
166165
}
167166

packages/cli/src/commands/memory.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import chalk from 'chalk';
21
import type { Command } from 'commander';
3-
import { memoryStoreCommand, memorySearchCommand } from '@ai-devkit/memory/api';
4-
import type { MemorySearchOptions, MemoryStoreOptions } from '@ai-devkit/memory/api';
2+
import { memoryStoreCommand, memorySearchCommand } from '@ai-devkit/memory';
3+
import type { MemorySearchOptions, MemoryStoreOptions } from '@ai-devkit/memory';
4+
import { ui } from '../util/terminal-ui';
55

66
export function registerMemoryCommand(program: Command): void {
77
const memoryCommand = program
@@ -21,7 +21,7 @@ export function registerMemoryCommand(program: Command): void {
2121
console.log(JSON.stringify(result, null, 2));
2222
} catch (error) {
2323
const message = error instanceof Error ? error.message : String(error);
24-
console.error(chalk.red(`[ERROR] ${message}`));
24+
ui.error(message);
2525
process.exit(1);
2626
}
2727
});
@@ -42,7 +42,7 @@ export function registerMemoryCommand(program: Command): void {
4242
console.log(JSON.stringify(result, null, 2));
4343
} catch (error) {
4444
const message = error instanceof Error ? error.message : String(error);
45-
console.error(chalk.red(`[ERROR] ${message}`));
45+
ui.error(message);
4646
process.exit(1);
4747
}
4848
});

packages/cli/src/commands/phase.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
import inquirer from 'inquirer';
2-
import chalk from 'chalk';
32
import { ConfigManager } from '../lib/Config';
43
import { TemplateManager } from '../lib/TemplateManager';
54
import { Phase, AVAILABLE_PHASES, PHASE_DISPLAY_NAMES } from '../types';
5+
import { ui } from '../util/terminal-ui';
66

77
export async function phaseCommand(phaseName?: string) {
88
const configManager = new ConfigManager();
99
const templateManager = new TemplateManager();
1010

1111
if (!(await configManager.exists())) {
12-
console.log(chalk.red('Error: AI DevKit not initialized. Run `ai-devkit init` first.'));
12+
ui.error('AI DevKit not initialized. Run `ai-devkit init` first.');
1313
return;
1414
}
1515

1616
let phase: Phase;
17-
17+
1818
if (phaseName && AVAILABLE_PHASES.includes(phaseName as Phase)) {
1919
phase = phaseName as Phase;
2020
} else if (phaseName) {
21-
console.log(chalk.red(`Error: Unknown phase "${phaseName}". Available phases: ${AVAILABLE_PHASES.join(', ')}`));
21+
ui.error(`Unknown phase "${phaseName}". Available phases: ${AVAILABLE_PHASES.join(', ')}`);
2222
return;
2323
} else {
2424
const config = await configManager.read();
2525
const availableToAdd = AVAILABLE_PHASES.filter(p => !config?.initializedPhases.includes(p));
2626

2727
if (availableToAdd.length === 0) {
28-
console.log(chalk.yellow('All phases are already initialized.'));
28+
ui.warning('All phases are already initialized.');
2929
const { shouldReinitialize } = await inquirer.prompt([
3030
{
3131
type: 'confirm',
@@ -70,14 +70,13 @@ export async function phaseCommand(phaseName?: string) {
7070
}
7171

7272
if (!shouldCopy) {
73-
console.log(chalk.yellow(`Cancelled adding ${phase} phase.`));
73+
ui.warning(`Cancelled adding ${phase} phase.`);
7474
return;
7575
}
7676

7777
const file = await templateManager.copyPhaseTemplate(phase);
7878
await configManager.addPhase(phase);
7979

80-
console.log(chalk.green(`\n[OK] ${PHASE_DISPLAY_NAMES[phase]} created successfully!`));
81-
console.log(chalk.blue(` Location: ${file}\n`));
82-
}
83-
80+
ui.success(`${PHASE_DISPLAY_NAMES[phase]} created successfully!`);
81+
ui.info(` Location: ${file}\n`);
82+
}

0 commit comments

Comments
 (0)