Skip to content

Commit e536524

Browse files
authored
cli charm view tree UI output (#1349)
feat(cli): display charm view as tree
1 parent 9c749ed commit e536524

File tree

2 files changed

+104
-18
lines changed

2 files changed

+104
-18
lines changed

packages/cli/commands/charm.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { Command, ValidationError } from "@cliffy/command";
33
import {
44
applyCharmInput,
55
CharmConfig,
6+
formatViewTree,
67
generateSpaceMap,
8+
getCharmView,
79
inspectCharm,
810
linkCharms,
911
listCharms,
@@ -229,6 +231,33 @@ Recipe: ${charmData.recipeName || "<no recipe name>"}
229231

230232
render(output);
231233
})
234+
/* charm view */
235+
.command("view", "Display the rendered view for a charm")
236+
.usage(charmUsage)
237+
.example(
238+
`ct charm view ${EX_ID} ${EX_COMP_CHARM}`,
239+
`Display the view for charm "${RAW_EX_COMP.charm!}".`,
240+
)
241+
.example(
242+
`ct charm view ${EX_ID} ${EX_URL}`,
243+
`Display the view for charm "${RAW_EX_COMP.charm!}".`,
244+
)
245+
.option("-c,--charm <charm:string>", "The target charm ID.")
246+
.option("--json", "Output raw JSON data")
247+
.action(async (options) => {
248+
const charmConfig = parseCharmOptions(options);
249+
const view = await getCharmView(charmConfig);
250+
if (options.json) {
251+
render(view ?? null, { json: true });
252+
return;
253+
}
254+
if (view) {
255+
const tree = formatViewTree(view);
256+
render(tree);
257+
} else {
258+
render("<no view data>");
259+
}
260+
})
232261
/* charm link */
233262
.command("link", "Link a field from one charm to another")
234263
.usage(spaceUsage)
@@ -299,7 +328,7 @@ Recipe: ${charmData.recipeName || "<no recipe name>"}
299328
.action(async (options) => {
300329
const spaceConfig = parseSpaceOptions(options);
301330
const format = options.format === "dot" ? MapFormat.DOT : MapFormat.ASCII;
302-
331+
303332
const map = await generateSpaceMap(spaceConfig, format);
304333
render(map);
305334
});

packages/cli/lib/charm.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
RecipeMeta,
99
Runtime,
1010
RuntimeProgram,
11+
UI,
1112
} from "@commontools/runner";
1213
import { StorageManager } from "@commontools/runner/storage/cache";
1314
import {
@@ -404,16 +405,22 @@ function createShortId(id: string): string {
404405

405406
function createCharmConnection(
406407
charm: { id: string; name?: string },
407-
details?: { name?: string; readingFrom: Array<{ id: string }>; readBy: Array<{ id: string }> },
408+
details?: {
409+
name?: string;
410+
readingFrom: Array<{ id: string }>;
411+
readBy: Array<{ id: string }>;
412+
},
408413
): CharmConnection {
409414
return {
410415
name: details?.name || charm.name || createShortId(charm.id),
411-
readingFrom: details?.readingFrom.map(c => c.id) || [],
412-
readBy: details?.readBy.map(c => c.id) || [],
416+
readingFrom: details?.readingFrom.map((c) => c.id) || [],
417+
readBy: details?.readBy.map((c) => c.id) || [],
413418
};
414419
}
415420

416-
async function buildConnectionMap(config: SpaceConfig): Promise<CharmConnectionMap> {
421+
async function buildConnectionMap(
422+
config: SpaceConfig,
423+
): Promise<CharmConnectionMap> {
417424
const charms = await listCharms(config);
418425
const connections: CharmConnectionMap = new Map();
419426

@@ -424,7 +431,11 @@ async function buildConnectionMap(config: SpaceConfig): Promise<CharmConnectionM
424431
connections.set(charm.id, createCharmConnection(charm, details));
425432
} catch (error) {
426433
// Skip charms that can't be inspected, but include them with no connections
427-
console.error(`Warning: Could not inspect charm ${charm.id}: ${error instanceof Error ? error.message : String(error)}`);
434+
console.error(
435+
`Warning: Could not inspect charm ${charm.id}: ${
436+
error instanceof Error ? error.message : String(error)
437+
}`,
438+
);
428439
connections.set(charm.id, createCharmConnection(charm));
429440
}
430441
}
@@ -441,35 +452,37 @@ function generateAsciiMap(connections: CharmConnectionMap): string {
441452

442453
// Sort charms by connection count for better visualization
443454
const sortedCharms = Array.from(connections.entries()).sort(
444-
([, a], [, b]) =>
445-
(b.readingFrom.length + b.readBy.length) -
446-
(a.readingFrom.length + a.readBy.length)
455+
([, a], [, b]) =>
456+
(b.readingFrom.length + b.readBy.length) -
457+
(a.readingFrom.length + a.readBy.length),
447458
);
448459

449460
for (const [id, info] of sortedCharms) {
450461
const shortId = createShortId(id);
451462
output += `📦 ${info.name} [${shortId}]\n`;
452-
463+
453464
if (info.readingFrom.length > 0) {
454465
output += " ← reads from:\n";
455466
for (const sourceId of info.readingFrom) {
456-
const sourceName = connections.get(sourceId)?.name || createShortId(sourceId);
467+
const sourceName = connections.get(sourceId)?.name ||
468+
createShortId(sourceId);
457469
output += ` • ${sourceName}\n`;
458470
}
459471
}
460-
472+
461473
if (info.readBy.length > 0) {
462474
output += " → read by:\n";
463475
for (const targetId of info.readBy) {
464-
const targetName = connections.get(targetId)?.name || createShortId(targetId);
476+
const targetName = connections.get(targetId)?.name ||
477+
createShortId(targetId);
465478
output += ` • ${targetName}\n`;
466479
}
467480
}
468-
481+
469482
if (info.readingFrom.length === 0 && info.readBy.length === 0) {
470483
output += " (no connections)\n";
471484
}
472-
485+
473486
output += "\n";
474487
}
475488

@@ -504,11 +517,16 @@ export enum MapFormat {
504517
DOT = "dot",
505518
}
506519

507-
export async function getCharmConnections(config: SpaceConfig): Promise<CharmConnectionMap> {
520+
export async function getCharmConnections(
521+
config: SpaceConfig,
522+
): Promise<CharmConnectionMap> {
508523
return await buildConnectionMap(config);
509524
}
510525

511-
export function formatSpaceMap(connections: CharmConnectionMap, format: MapFormat): string {
526+
export function formatSpaceMap(
527+
connections: CharmConnectionMap,
528+
format: MapFormat,
529+
): string {
512530
switch (format) {
513531
case MapFormat.ASCII:
514532
return generateAsciiMap(connections);
@@ -519,7 +537,10 @@ export function formatSpaceMap(connections: CharmConnectionMap, format: MapForma
519537
}
520538
}
521539

522-
export async function generateSpaceMap(config: SpaceConfig, format: MapFormat = MapFormat.ASCII): Promise<string> {
540+
export async function generateSpaceMap(
541+
config: SpaceConfig,
542+
format: MapFormat = MapFormat.ASCII,
543+
): Promise<string> {
523544
const connections = await getCharmConnections(config);
524545
return formatSpaceMap(connections, format);
525546
}
@@ -578,3 +599,39 @@ export async function inspectCharm(
578599
readBy,
579600
};
580601
}
602+
603+
export async function getCharmView(
604+
config: CharmConfig,
605+
): Promise<unknown> {
606+
const data = await inspectCharm(config);
607+
return data.result?.[UI];
608+
}
609+
610+
export function formatViewTree(view: unknown): string {
611+
const isVNode = (v: any): v is { name: string; children: any[] } => {
612+
return v && typeof v === "object" && v.type === "vnode" && v.name;
613+
};
614+
615+
const format = (
616+
node: unknown,
617+
prefix: string,
618+
last: boolean,
619+
): string => {
620+
const branch = last ? "└─ " : "├─ ";
621+
if (!isVNode(node)) {
622+
return `${prefix}${branch}${String(node)}`;
623+
}
624+
625+
const children = Array.isArray(node.children) ? node.children : [];
626+
let output = `${prefix}${branch}${node.name}`;
627+
const nextPrefix = prefix + (last ? " " : "│ ");
628+
for (let i = 0; i < children.length; i++) {
629+
const child = children[i];
630+
const isLast = i === children.length - 1;
631+
output += "\n" + format(child, nextPrefix, isLast);
632+
}
633+
return output;
634+
};
635+
636+
return format(view, "", true);
637+
}

0 commit comments

Comments
 (0)