- 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>
147 lines
4.8 KiB
TypeScript
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));
|
|
}
|
|
});
|
|
}
|