feat: add get and describe commands with API client
kubectl-style get (table/json/yaml) and describe commands for servers, profiles, projects, instances. ApiClient for daemon communication. 118 CLI tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
85
src/cli/tests/commands/describe.test.ts
Normal file
85
src/cli/tests/commands/describe.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createDescribeCommand } from '../../src/commands/describe.js';
|
||||
import type { DescribeCommandDeps } from '../../src/commands/describe.js';
|
||||
|
||||
function makeDeps(item: unknown = {}): DescribeCommandDeps & { output: string[] } {
|
||||
const output: string[] = [];
|
||||
return {
|
||||
output,
|
||||
fetchResource: vi.fn(async () => item),
|
||||
log: (...args: string[]) => output.push(args.join(' ')),
|
||||
};
|
||||
}
|
||||
|
||||
describe('describe command', () => {
|
||||
it('shows detailed server info', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'srv-1',
|
||||
name: 'slack',
|
||||
transport: 'STDIO',
|
||||
packageName: '@slack/mcp',
|
||||
dockerImage: null,
|
||||
envTemplate: [],
|
||||
});
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'server', 'srv-1']);
|
||||
|
||||
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1');
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('--- Server ---');
|
||||
expect(text).toContain('name: slack');
|
||||
expect(text).toContain('transport: STDIO');
|
||||
expect(text).toContain('dockerImage: -');
|
||||
});
|
||||
|
||||
it('resolves resource aliases', async () => {
|
||||
const deps = makeDeps({ id: 'p1' });
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'prof', 'p1']);
|
||||
expect(deps.fetchResource).toHaveBeenCalledWith('profiles', 'p1');
|
||||
});
|
||||
|
||||
it('outputs JSON format', async () => {
|
||||
const deps = makeDeps({ id: 'srv-1', name: 'slack' });
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'server', 'srv-1', '-o', 'json']);
|
||||
|
||||
const parsed = JSON.parse(deps.output[0] ?? '');
|
||||
expect(parsed.name).toBe('slack');
|
||||
});
|
||||
|
||||
it('outputs YAML format', async () => {
|
||||
const deps = makeDeps({ id: 'srv-1', name: 'slack' });
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'server', 'srv-1', '-o', 'yaml']);
|
||||
expect(deps.output[0]).toContain('name: slack');
|
||||
});
|
||||
|
||||
it('formats nested objects', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'srv-1',
|
||||
name: 'slack',
|
||||
metadata: { version: '1.0', nested: { deep: true } },
|
||||
});
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'server', 'srv-1']);
|
||||
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('metadata:');
|
||||
expect(text).toContain('version: 1.0');
|
||||
});
|
||||
|
||||
it('formats arrays correctly', async () => {
|
||||
const deps = makeDeps({
|
||||
id: 'srv-1',
|
||||
permissions: ['read', 'write'],
|
||||
envTemplate: [],
|
||||
});
|
||||
const cmd = createDescribeCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'server', 'srv-1']);
|
||||
|
||||
const text = deps.output.join('\n');
|
||||
expect(text).toContain('permissions: read, write');
|
||||
expect(text).toContain('envTemplate: []');
|
||||
});
|
||||
});
|
||||
86
src/cli/tests/commands/get.test.ts
Normal file
86
src/cli/tests/commands/get.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createGetCommand } from '../../src/commands/get.js';
|
||||
import type { GetCommandDeps } from '../../src/commands/get.js';
|
||||
|
||||
function makeDeps(items: unknown[] = []): GetCommandDeps & { output: string[] } {
|
||||
const output: string[] = [];
|
||||
return {
|
||||
output,
|
||||
fetchResource: vi.fn(async () => items),
|
||||
log: (...args: string[]) => output.push(args.join(' ')),
|
||||
};
|
||||
}
|
||||
|
||||
describe('get command', () => {
|
||||
it('lists servers in table format', async () => {
|
||||
const deps = makeDeps([
|
||||
{ id: 'srv-1', name: 'slack', transport: 'STDIO', packageName: '@slack/mcp', dockerImage: null },
|
||||
{ id: 'srv-2', name: 'github', transport: 'SSE', packageName: null, dockerImage: 'ghcr.io/github-mcp' },
|
||||
]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'servers']);
|
||||
|
||||
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
|
||||
expect(deps.output[0]).toContain('NAME');
|
||||
expect(deps.output[0]).toContain('TRANSPORT');
|
||||
expect(deps.output.join('\n')).toContain('slack');
|
||||
expect(deps.output.join('\n')).toContain('github');
|
||||
});
|
||||
|
||||
it('resolves resource aliases', async () => {
|
||||
const deps = makeDeps([]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'srv']);
|
||||
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
|
||||
});
|
||||
|
||||
it('passes ID when provided', async () => {
|
||||
const deps = makeDeps([{ id: 'srv-1', name: 'slack' }]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'servers', 'srv-1']);
|
||||
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1');
|
||||
});
|
||||
|
||||
it('outputs JSON format', async () => {
|
||||
const deps = makeDeps([{ id: 'srv-1', name: 'slack' }]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'json']);
|
||||
|
||||
const parsed = JSON.parse(deps.output[0] ?? '');
|
||||
expect(parsed).toEqual({ id: 'srv-1', name: 'slack' });
|
||||
});
|
||||
|
||||
it('outputs YAML format', async () => {
|
||||
const deps = makeDeps([{ id: 'srv-1', name: 'slack' }]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'yaml']);
|
||||
expect(deps.output[0]).toContain('name: slack');
|
||||
});
|
||||
|
||||
it('lists profiles with correct columns', async () => {
|
||||
const deps = makeDeps([
|
||||
{ id: 'p1', name: 'default', serverId: 'srv-1' },
|
||||
]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'profiles']);
|
||||
expect(deps.output[0]).toContain('NAME');
|
||||
expect(deps.output[0]).toContain('SERVER ID');
|
||||
});
|
||||
|
||||
it('lists instances with correct columns', async () => {
|
||||
const deps = makeDeps([
|
||||
{ id: 'inst-1', serverId: 'srv-1', status: 'RUNNING', containerId: 'abc123def456', port: 3000 },
|
||||
]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'instances']);
|
||||
expect(deps.output[0]).toContain('STATUS');
|
||||
expect(deps.output.join('\n')).toContain('RUNNING');
|
||||
});
|
||||
|
||||
it('shows no results message for empty list', async () => {
|
||||
const deps = makeDeps([]);
|
||||
const cmd = createGetCommand(deps);
|
||||
await cmd.parseAsync(['node', 'test', 'servers']);
|
||||
expect(deps.output[0]).toContain('No results');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user