Skip to content

Commit d8b3c1f

Browse files
Copilotfuxingloh
andcommitted
feat(USE-62): add --output human format with boxen rendering
Co-authored-by: fuxingloh <4266087+fuxingloh@users.noreply.github.com>
1 parent 2b56dce commit d8b3c1f

File tree

8 files changed

+82
-13
lines changed

8 files changed

+82
-13
lines changed

packages/use-agently/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ cli
2121
"Agently is the way AI coordinate and transact. The routing and settlement layer for your agent economy.",
2222
)
2323
.version(pkg.version)
24-
.option("-o, --output <format>", "Output format (text, json)", process.stdout.isTTY ? "text" : "json")
24+
.option("-o, --output <format>", "Output format (human, yaml, json)", process.stdout.isTTY ? "human" : "json")
2525
.argument("[args...]")
2626
.action((args: string[]) => {
2727
if (args.length > 0) {

packages/use-agently/src/commands/agents.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ describe("agents command", () => {
6767
expect(Object.keys(lines[0])).toEqual(["id", "name", "description"]);
6868
});
6969

70+
test("human output renders items with separator", async () => {
71+
await cli.parseAsync(["test", "use-agently", "-o", "human", "agents"]);
72+
73+
const rendered = out.stdout;
74+
expect(rendered).toContain("Test Agent");
75+
expect(rendered).toContain("Another Agent");
76+
// items are separated by a horizontal rule
77+
expect(rendered).toContain("─");
78+
});
79+
80+
test("human output for empty list renders '(no results)'", async () => {
81+
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ hits: [], found: 0, page: 1, per_page: 20 })));
82+
await cli.parseAsync(["test", "use-agently", "-o", "human", "agents"]);
83+
84+
expect(out.stdout).toBe("(no results)");
85+
});
86+
7087
test("empty agents list", async () => {
7188
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ hits: [], found: 0, page: 1, per_page: 20 })));
7289
await cli.parseAsync(["test", "use-agently", "-o", "json", "agents"]);

packages/use-agently/src/commands/doctor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe("doctor command", () => {
3838
});
3939

4040
test("all checks pass - text output", async () => {
41-
await cli.parseAsync(["test", "use-agently", "doctor", "--rpc", fixture.container.getRpcUrl()]);
41+
await cli.parseAsync(["test", "use-agently", "-o", "text", "doctor", "--rpc", fixture.container.getRpcUrl()]);
4242

4343
expect(out.yaml).toEqual({
4444
ok: true,

packages/use-agently/src/commands/init.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("init command", () => {
4545
});
4646

4747
test("text output on new wallet", async () => {
48-
await cli.parseAsync(["test", "use-agently", "init"]);
48+
await cli.parseAsync(["test", "use-agently", "-o", "text", "init"]);
4949

5050
expect(out.yaml).toEqual({
5151
address: TEST_ADDRESS,

packages/use-agently/src/commands/update.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe("update command", () => {
5555
});
5656

5757
test("text output - update available", async () => {
58-
await cli.parseAsync(["test", "use-agently", "update"]);
58+
await cli.parseAsync(["test", "use-agently", "-o", "text", "update"]);
5959

6060
expect(out.yaml).toEqual({
6161
current: CURRENT_VERSION,

packages/use-agently/src/commands/view.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe("view command", () => {
3333

3434
test("displays agent details", async () => {
3535
fetchSpy = spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(TEST_AGENT)));
36-
await cli.parseAsync(["test", "use-agently", "view", "eip155:8453/erc8004:0x1234/1"]);
36+
await cli.parseAsync(["test", "use-agently", "-o", "text", "view", "eip155:8453/erc8004:0x1234/1"]);
3737
const parsed = out.yaml as Record<string, unknown>;
3838
expect(parsed).toHaveProperty("name", "Echo Agent");
3939
expect(parsed).toHaveProperty("id", "eip155:8453/erc8004:0x1234/1");

packages/use-agently/src/commands/whoami.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe("whoami command", () => {
99
const out = captureOutput();
1010

1111
test("text output", async () => {
12-
await cli.parseAsync(["test", "use-agently", "whoami"]);
12+
await cli.parseAsync(["test", "use-agently", "-o", "text", "whoami"]);
1313

1414
expect(out.yaml).toEqual({
1515
namespace: "eip155",
@@ -25,4 +25,25 @@ describe("whoami command", () => {
2525
address: TEST_ADDRESS,
2626
});
2727
});
28+
29+
test("yaml output", async () => {
30+
await cli.parseAsync(["test", "use-agently", "-o", "yaml", "whoami"]);
31+
32+
expect(out.yaml).toEqual({
33+
namespace: "eip155",
34+
address: TEST_ADDRESS,
35+
});
36+
});
37+
38+
test("human output renders a box containing the data", async () => {
39+
await cli.parseAsync(["test", "use-agently", "-o", "human", "whoami"]);
40+
41+
const rendered = out.stdout;
42+
expect(rendered).toContain("namespace");
43+
expect(rendered).toContain("eip155");
44+
expect(rendered).toContain("address");
45+
expect(rendered).toContain(TEST_ADDRESS);
46+
// boxen uses rounded corners
47+
expect(rendered).toMatch(/[]/);
48+
});
2849
});

packages/use-agently/src/output.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,61 @@
11
import { Command } from "commander";
22
import { stringify } from "yaml";
3+
import boxen from "boxen";
34

4-
export type OutputFormat = "json" | "text";
5+
export type OutputFormat = "json" | "yaml" | "text" | "human";
56

6-
export function renderText(data: unknown): string {
7+
export function renderYaml(data: unknown): string {
78
return stringify(data, { lineWidth: 0 }).trimEnd();
89
}
910

11+
/** @deprecated use renderYaml */
12+
export const renderText = renderYaml;
13+
14+
/**
15+
* Render a single value for human consumption.
16+
* Strings are returned as-is; objects/arrays are wrapped in a rounded boxen box.
17+
*/
18+
export function renderHuman(data: unknown): string {
19+
if (typeof data === "string") return data;
20+
return boxen(renderYaml(data), { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: "round" });
21+
}
22+
23+
/**
24+
* Render a collection for human consumption.
25+
* Each item is formatted as YAML and separated by a horizontal rule.
26+
*/
27+
export function renderHumanCollection(items: unknown[]): string {
28+
if (items.length === 0) return "(no results)";
29+
const separator = "─".repeat(40);
30+
return items.map((item) => (typeof item === "string" ? item : renderYaml(item))).join(`\n${separator}\n`);
31+
}
32+
1033
export function output(command: Command, data: unknown): void {
11-
if (command.optsWithGlobals().output === "json") {
34+
const format: OutputFormat = command.optsWithGlobals().output ?? "human";
35+
if (format === "json") {
1236
console.log(JSON.stringify(data));
37+
} else if (format === "human") {
38+
console.log(renderHuman(data));
1339
} else {
14-
console.log(renderText(data));
40+
// "yaml" or "text" (backward-compat alias)
41+
console.log(renderYaml(data));
1542
}
1643
}
1744

1845
/**
1946
* Output a collection as NDJSON (one JSON object per line) in JSON mode,
20-
* or as YAML text in text mode.
47+
* as YAML in yaml/text mode, or as a human-readable list in human mode.
2148
*/
2249
export function outputCollection(command: Command, items: unknown[]): void {
23-
if (command.optsWithGlobals().output === "json") {
50+
const format: OutputFormat = command.optsWithGlobals().output ?? "human";
51+
if (format === "json") {
2452
for (const item of items) {
2553
console.log(JSON.stringify(item));
2654
}
55+
} else if (format === "human") {
56+
console.log(renderHumanCollection(items));
2757
} else {
28-
console.log(renderText(items));
58+
// "yaml" or "text" (backward-compat alias)
59+
console.log(renderYaml(items));
2960
}
3061
}

0 commit comments

Comments
 (0)