Skip to content

Commit add4b3c

Browse files
committed
fix(v1-review): format CLI tables
1 parent 42fa829 commit add4b3c

12 files changed

Lines changed: 157 additions & 44 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codealmanac",
3-
"version": "0.2.7",
3+
"version": "0.2.8",
44
"description": "A living wiki for codebases, maintained by AI agents. Documents what the code can't say: decisions, flows, invariants, incidents, gotchas.",
55
"keywords": [
66
"wiki",
@@ -22,9 +22,9 @@
2222
},
2323
"type": "module",
2424
"bin": {
25-
"codealmanac": "./dist/codealmanac.js",
26-
"almanac": "./dist/codealmanac.js",
27-
"alm": "./dist/codealmanac.js"
25+
"codealmanac": "dist/codealmanac.js",
26+
"almanac": "dist/codealmanac.js",
27+
"alm": "dist/codealmanac.js"
2828
},
2929
"files": [
3030
"dist",

src/commands/agents.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,40 @@ import {
22
buildProviderSetupView,
33
parseAgentSelection,
44
type ProviderReadiness,
5+
type ProviderSetupView,
56
} from "../agent/provider-view.js";
67
import {
78
isAgentProviderId,
89
readConfig,
910
writeConfig,
1011
type AgentProviderId,
1112
} from "../update/config.js";
13+
import { formatTextTable } from "./table.js";
1214

1315
export interface AgentsResult {
1416
stdout: string;
1517
stderr: string;
1618
exitCode: number;
1719
}
1820

19-
export async function runAgentsList(): Promise<AgentsResult> {
20-
const view = await buildProviderSetupView();
21+
export async function runAgentsList(opts: {
22+
view?: ProviderSetupView;
23+
} = {}): Promise<AgentsResult> {
24+
const view = opts.view ?? await buildProviderSetupView();
2125
const lines = ["codealmanac agents\n"];
22-
for (const choice of view.choices) {
23-
const selected = choice.selected ? "*" : " ";
24-
const recommended = choice.recommended ? "recommended" : "";
25-
const model = choice.effectiveModel ?? "provider default";
26-
const detail = choice.account ?? choice.fixCommand ?? choice.detail;
27-
lines.push(
28-
[
29-
selected,
30-
choice.label.padEnd(6),
31-
readinessLabel(choice.readiness).padEnd(15),
32-
recommended.padEnd(11),
33-
`model: ${model}`.padEnd(31),
34-
detail,
35-
].join(" ").trimEnd(),
36-
);
37-
}
26+
lines.push(
27+
...formatTextTable({
28+
headers: ["DEFAULT", "AGENT", "STATUS", "RECOMMENDED", "MODEL", "DETAIL"],
29+
rows: view.choices.map((choice) => [
30+
choice.selected ? "*" : "",
31+
choice.label,
32+
readinessLabel(choice.readiness),
33+
choice.recommended ? "recommended" : "",
34+
choice.effectiveModel ?? "provider default",
35+
choice.account ?? choice.fixCommand ?? choice.detail,
36+
]),
37+
}),
38+
);
3839
lines.push(
3940
"\nUse: almanac agents use <claude|codex|cursor>",
4041
"Set model: almanac agents model <provider> <model>",

src/commands/config.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
readConfigWithOrigins,
1919
serializeConfig,
2020
} from "../update/config.js";
21+
import { formatTextTable } from "./table.js";
2122

2223
export interface ConfigResult {
2324
stdout: string;
@@ -37,11 +38,16 @@ export async function runConfigList(opts: {
3738
if (opts.json === true) {
3839
return ok(`${JSON.stringify(rows, null, 2)}\n`);
3940
}
40-
const lines = rows.map((row) => {
41-
const value = formatConfigValue(row.value);
42-
return opts.showOrigin === true
43-
? `${row.key.padEnd(20)} ${value.padEnd(24)} ${row.origin}`
44-
: `${row.key.padEnd(20)} ${value}`;
41+
const lines = formatTextTable({
42+
headers: opts.showOrigin === true
43+
? ["KEY", "VALUE", "ORIGIN"]
44+
: ["KEY", "VALUE"],
45+
rows: rows.map((row) => {
46+
const value = formatConfigValue(row.value);
47+
return opts.showOrigin === true
48+
? [row.key, value, row.origin]
49+
: [row.key, value];
50+
}),
4551
});
4652
return ok(`${lines.join("\n")}\n`);
4753
}

src/commands/jobs.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
33
import type { CommandResult } from "../cli/helpers.js";
44
import { renderOutcome } from "../cli/outcome.js";
55
import { findNearestAlmanacDir } from "../paths.js";
6+
import { formatTextTable } from "./table.js";
67
import {
78
finishRunRecord,
89
markRunCancelled,
@@ -48,12 +49,7 @@ export async function runJobsList(
4849
if (views.length === 0) {
4950
return { stdout: "Jobs\n\nNo jobs found.\n", stderr: "", exitCode: 0 };
5051
}
51-
const lines = ["Jobs", ""];
52-
for (const view of views) {
53-
lines.push(
54-
`${view.id} ${view.operation} ${view.displayStatus} ${formatMs(view.elapsedMs)}`,
55-
);
56-
}
52+
const lines = ["Jobs", "", ...formatJobRows(views)];
5753
return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 };
5854
}
5955

@@ -301,6 +297,18 @@ function formatMs(ms: number): string {
301297
return `${Math.round(minutes / 60)}h`;
302298
}
303299

300+
function formatJobRows(views: RunView[]): string[] {
301+
return formatTextTable({
302+
headers: ["ID", "OPERATION", "STATUS", "ELAPSED"],
303+
rows: views.map((view) => [
304+
view.id,
305+
view.operation,
306+
view.displayStatus,
307+
formatMs(view.elapsedMs),
308+
]),
309+
});
310+
}
311+
304312
function terminalAttachSummary(view: RunView): string {
305313
if (view.displayStatus !== "failed" && view.displayStatus !== "stale") {
306314
return "";

src/commands/table.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export interface TextTable {
2+
headers: string[];
3+
rows: string[][];
4+
}
5+
6+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
7+
8+
export function formatTextTable(table: TextTable): string[] {
9+
if (table.headers.length === 0) return [];
10+
const widths = table.headers.map((header, index) =>
11+
Math.max(
12+
visibleLength(header),
13+
...table.rows.map((row) => visibleLength(row[index] ?? "")),
14+
),
15+
);
16+
return [
17+
formatTableRow(table.headers, widths),
18+
...table.rows.map((row) => formatTableRow(row, widths)),
19+
];
20+
}
21+
22+
function formatTableRow(row: string[], widths: number[]): string {
23+
return widths
24+
.map((width, index) => padVisible(row[index] ?? "", width))
25+
.join(" ")
26+
.trimEnd();
27+
}
28+
29+
function padVisible(value: string, width: number): string {
30+
return `${value}${" ".repeat(Math.max(0, width - visibleLength(value)))}`;
31+
}
32+
33+
function visibleLength(value: string): number {
34+
return value.replace(ANSI_RE, "").length;
35+
}

src/commands/topics/list.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ensureFreshIndex } from "../../indexer/index.js";
33
import { resolveWikiRoot } from "../../indexer/resolve-wiki.js";
44
import { openIndex } from "../../indexer/schema.js";
55
import { indexDbPath } from "../../topics/paths.js";
6+
import { formatTextTable } from "../table.js";
67
import type { TopicsCommandOutput, TopicsListOptions } from "./types.js";
78

89
/**
@@ -57,11 +58,12 @@ export async function runTopicsList(
5758
};
5859
}
5960

60-
const slugWidth = rows.reduce((w, r) => Math.max(w, r.slug.length), 0);
61-
const lines = rows.map((r) => {
62-
const slug = r.slug.padEnd(slugWidth);
63-
const count = `(${r.page_count} page${r.page_count === 1 ? "" : "s"})`;
64-
return `${BLUE}${slug}${RST} ${DIM}${count}${RST}`;
61+
const lines = formatTextTable({
62+
headers: ["TOPIC", "PAGES"],
63+
rows: rows.map((r) => {
64+
const count = `(${r.page_count} page${r.page_count === 1 ? "" : "s"})`;
65+
return [`${BLUE}${r.slug}${RST}`, `${DIM}${count}${RST}`];
66+
}),
6567
});
6668
return { stdout: `${lines.join("\n")}\n`, stderr: "", exitCode: 0 };
6769
} finally {

src/harness/providers/codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ export async function runCodexAppServer(
594594
clientInfo: {
595595
name: "codealmanac",
596596
title: "Code Almanac",
597-
version: "0.2.7",
597+
version: "0.2.8",
598598
},
599599
capabilities: {
600600
experimentalApi: true,

test/agents-command.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,65 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { runAgentsModel, runAgentsUse } from "../src/commands/agents.js";
3+
import { runAgentsList, runAgentsModel, runAgentsUse } from "../src/commands/agents.js";
44
import { runConfigList } from "../src/commands/config.js";
55
import { withTempHome } from "./helpers.js";
66

77
describe("agents command", () => {
8+
it("lists providers with column headers", async () => {
9+
const result = await runAgentsList({
10+
view: {
11+
defaultProvider: "codex",
12+
recommendedProvider: "codex",
13+
choices: [
14+
{
15+
id: "codex",
16+
label: "Codex",
17+
selected: true,
18+
recommended: true,
19+
readiness: "ready",
20+
ready: true,
21+
installed: true,
22+
authenticated: true,
23+
effectiveModel: null,
24+
providerDefaultModel: null,
25+
configuredModel: null,
26+
account: "user@example.com",
27+
detail: "Logged in",
28+
fixCommand: null,
29+
modelChoices: [],
30+
},
31+
{
32+
id: "cursor",
33+
label: "Cursor",
34+
selected: false,
35+
recommended: false,
36+
readiness: "missing",
37+
ready: false,
38+
installed: false,
39+
authenticated: false,
40+
effectiveModel: null,
41+
providerDefaultModel: null,
42+
configuredModel: null,
43+
account: null,
44+
detail: "missing",
45+
fixCommand: "install cursor-agent",
46+
modelChoices: [],
47+
},
48+
],
49+
},
50+
});
51+
52+
expect(result.stdout).toContain(
53+
"DEFAULT AGENT STATUS RECOMMENDED MODEL DETAIL",
54+
);
55+
expect(result.stdout).toContain(
56+
"* Codex ready recommended provider default user@example.com",
57+
);
58+
expect(result.stdout).toContain(
59+
" Cursor missing provider default install cursor-agent",
60+
);
61+
});
62+
863
it("requires an explicit model or --default", async () => {
964
await withTempHome(async () => {
1065
const result = await runAgentsModel({ provider: "claude" });

test/config-command.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe("config command", () => {
1818
const result = await runConfigList({ showOrigin: true });
1919

2020
expect(result.exitCode).toBe(0);
21+
expect(result.stdout).toContain("KEY VALUE ORIGIN");
2122
expect(result.stdout).toContain("agent.default");
2223
expect(result.stdout).toContain("agent.models.claude");
2324
expect(result.stdout).toContain("default");

0 commit comments

Comments
 (0)