Files
mcpctl/src/cli/src/commands/get.ts
Michal 4c127a7dc3
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled
fix: show server name in instances table, allow logs by server name
- Instance list now shows server NAME instead of cryptic server ID
- Include server relation in findAll query (Prisma include)
- Logs command accepts server name, server ID, or instance ID
  (resolves server name → first RUNNING instance)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:07:42 +00:00

147 lines
4.8 KiB
TypeScript

import { Command } from 'commander';
import { formatTable } from '../formatters/table.js';
import { formatJson, formatYaml } from '../formatters/output.js';
import type { Column } from '../formatters/table.js';
import { resolveResource, stripInternalFields } from './shared.js';
export interface GetCommandDeps {
fetchResource: (resource: string, id?: string) => Promise<unknown[]>;
log: (...args: string[]) => void;
}
interface ServerRow {
id: string;
name: string;
transport: string;
packageName: string | null;
dockerImage: string | null;
}
interface ProjectRow {
id: string;
name: string;
description: string;
ownerId: string;
}
interface SecretRow {
id: string;
name: string;
data: Record<string, string>;
}
interface TemplateRow {
id: string;
name: string;
version: string;
transport: string;
packageName: string | null;
description: string;
}
interface InstanceRow {
id: string;
serverId: string;
server?: { name: string };
status: string;
containerId: string | null;
port: number | null;
healthStatus: string | null;
}
const serverColumns: Column<ServerRow>[] = [
{ header: 'NAME', key: 'name' },
{ header: 'TRANSPORT', key: 'transport', width: 16 },
{ header: 'PACKAGE', key: (r) => r.packageName ?? '-' },
{ header: 'IMAGE', key: (r) => r.dockerImage ?? '-' },
{ header: 'ID', key: 'id' },
];
const projectColumns: Column<ProjectRow>[] = [
{ header: 'NAME', key: 'name' },
{ header: 'DESCRIPTION', key: 'description', width: 40 },
{ header: 'OWNER', key: 'ownerId' },
{ header: 'ID', key: 'id' },
];
const secretColumns: Column<SecretRow>[] = [
{ header: 'NAME', key: 'name' },
{ header: 'KEYS', key: (r) => Object.keys(r.data).join(', ') || '-', width: 40 },
{ header: 'ID', key: 'id' },
];
const templateColumns: Column<TemplateRow>[] = [
{ header: 'NAME', key: 'name' },
{ header: 'VERSION', key: 'version', width: 10 },
{ header: 'TRANSPORT', key: 'transport', width: 16 },
{ header: 'PACKAGE', key: (r) => r.packageName ?? '-' },
{ header: 'DESCRIPTION', key: 'description', width: 50 },
];
const instanceColumns: Column<InstanceRow>[] = [
{ header: 'NAME', key: (r) => r.server?.name ?? '-', width: 20 },
{ header: 'STATUS', key: 'status', width: 10 },
{ header: 'HEALTH', key: (r) => r.healthStatus ?? '-', width: 10 },
{ header: 'PORT', key: (r) => r.port != null ? String(r.port) : '-', width: 6 },
{ header: 'CONTAINER', key: (r) => r.containerId ? r.containerId.slice(0, 12) : '-', width: 14 },
{ header: 'ID', key: 'id' },
];
function getColumnsForResource(resource: string): Column<Record<string, unknown>>[] {
switch (resource) {
case 'servers':
return serverColumns as unknown as Column<Record<string, unknown>>[];
case 'projects':
return projectColumns as unknown as Column<Record<string, unknown>>[];
case 'secrets':
return secretColumns as unknown as Column<Record<string, unknown>>[];
case 'templates':
return templateColumns as unknown as Column<Record<string, unknown>>[];
case 'instances':
return instanceColumns as unknown as Column<Record<string, unknown>>[];
default:
return [
{ header: 'ID', key: 'id' as keyof Record<string, unknown> },
{ header: 'NAME', key: 'name' as keyof Record<string, unknown> },
];
}
}
/**
* Transform API response items into apply-compatible format.
* Strips internal fields and wraps in the resource key.
*/
function toApplyFormat(resource: string, items: unknown[]): Record<string, unknown[]> {
const cleaned = items.map((item) => {
return stripInternalFields(item as Record<string, unknown>);
});
return { [resource]: cleaned };
}
export function createGetCommand(deps: GetCommandDeps): Command {
return new Command('get')
.description('List resources (servers, projects, instances)')
.argument('<resource>', 'resource type (servers, projects, instances)')
.argument('[id]', 'specific resource ID or name')
.option('-o, --output <format>', 'output format (table, json, yaml)', 'table')
.action(async (resourceArg: string, id: string | undefined, opts: { output: string }) => {
const resource = resolveResource(resourceArg);
const items = await deps.fetchResource(resource, id);
if (opts.output === 'json') {
// Apply-compatible JSON wrapped in resource key
deps.log(formatJson(toApplyFormat(resource, items)));
} else if (opts.output === 'yaml') {
// Apply-compatible YAML wrapped in resource key
deps.log(formatYaml(toApplyFormat(resource, items)));
} else {
if (items.length === 0) {
deps.log(`No ${resource} found.`);
return;
}
const columns = getColumnsForResource(resource);
deps.log(formatTable(items as Record<string, unknown>[], columns));
}
});
}