feat(table): add header spec support and explicit auto mode

This commit is contained in:
2026-02-08 14:17:20 +01:00
parent 861b509b15
commit 2661e4dc5b
2 changed files with 85 additions and 12 deletions

View File

@@ -22,7 +22,7 @@ Commands:
list-apps [--display-name|-n <name>] list-apps [--display-name|-n <name>]
list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s]
list-app-grants --app-id|-i <appId> list-app-grants --app-id|-i <appId>
table [--pretty|-p] [--quote-guids|-g] table [--pretty|-p] [--quote-guids|-g] [--header|-H <spec|auto|a>]
Options: Options:
-n, --display-name <name> Filter apps by exact display name -n, --display-name <name> Filter apps by exact display name
@@ -32,6 +32,7 @@ Options:
-q, --query <jmespath> Filter output JSON using JMESPath -q, --query <jmespath> Filter output JSON using JMESPath
-p, --pretty Use normalized column widths for Markdown table output -p, --pretty Use normalized column widths for Markdown table output
-g, --quote-guids In pretty tables, wrap GUID values in backticks -g, --quote-guids In pretty tables, wrap GUID values in backticks
-H, --header <value> Header mode/spec: auto|a OR "col1, col2" OR "key1: Label 1, key2: Label 2"
-o, --output <format> Output format: json|table|prettytable (default: json) -o, --output <format> Output format: json|table|prettytable (default: json)
-h, --help Show this help message`; -h, --help Show this help message`;
} }
@@ -42,6 +43,40 @@ function outputFiltered(object, query) {
: object; : object;
} }
function parseHeaderSpec(headerValue) {
if (!headerValue) {
return { mode: "default" };
}
const raw = headerValue.trim();
if (raw === "" || raw.toLowerCase() === "auto" || raw.toLowerCase() === "a") {
return { mode: "auto" };
}
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
const isMap = parts.some((p) => p.includes(":"));
if (!isMap) {
return { mode: "list", labels: parts };
}
const map = {};
for (const part of parts) {
const idx = part.indexOf(":");
if (idx < 0) {
throw new Error(`Invalid --header mapping segment: '${part}'`);
}
const key = part.slice(0, idx).trim();
const label = part.slice(idx + 1).trim();
if (!key || !label) {
throw new Error(`Invalid --header mapping segment: '${part}'`);
}
map[key] = label;
}
return { mode: "map", map };
}
function omitPermissionGuidColumns(value) { function omitPermissionGuidColumns(value) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map((item) => omitPermissionGuidColumns(item)); return value.map((item) => omitPermissionGuidColumns(item));
@@ -97,6 +132,7 @@ async function main() {
query: { type: "string", short: "q" }, query: { type: "string", short: "q" },
pretty: { type: "boolean", short: "p" }, pretty: { type: "boolean", short: "p" },
"quote-guids": { type: "boolean", short: "g" }, "quote-guids": { type: "boolean", short: "g" },
header: { type: "string", short: "H" },
output: { type: "string", short: "o" }, output: { type: "string", short: "o" },
}, },
strict: true, strict: true,
@@ -165,12 +201,23 @@ async function main() {
const output = command === "list-app-permissions" && values.short const output = command === "list-app-permissions" && values.short
? omitPermissionGuidColumns(filtered) ? omitPermissionGuidColumns(filtered)
: filtered; : filtered;
const headerSpec = parseHeaderSpec(values.header);
if (command === "table") { if (command === "table") {
console.log(toMarkdownTable(output, Boolean(values.pretty), Boolean(values["quote-guids"]))); console.log(toMarkdownTable(
output,
Boolean(values.pretty),
Boolean(values["quote-guids"]),
headerSpec,
));
} else if (outputFormat === "prettytable") { } else if (outputFormat === "prettytable") {
console.log(toMarkdownTable(output, true, Boolean(values["quote-guids"]))); console.log(toMarkdownTable(
output,
true,
Boolean(values["quote-guids"]),
headerSpec,
));
} else if (outputFormat === "table") { } else if (outputFormat === "table") {
console.log(toMarkdownTable(output)); console.log(toMarkdownTable(output, false, false, headerSpec));
} else { } else {
console.log(JSON.stringify(output, null, 2)); console.log(JSON.stringify(output, null, 2));
} }

View File

@@ -12,6 +12,19 @@ function isGuid(value) {
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
} }
function toAutoHeaderLabel(key) {
const withSpaces = String(key)
.replace(/[_-]+/g, " ")
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/\s+/g, " ")
.trim();
return withSpaces
.split(" ")
.filter(Boolean)
.map((part) => part[0].toUpperCase() + part.slice(1))
.join(" ");
}
function getScalarRowsAndHeaders(value) { function getScalarRowsAndHeaders(value) {
let rows; let rows;
if (Array.isArray(value)) { if (Array.isArray(value)) {
@@ -52,35 +65,48 @@ function getScalarRowsAndHeaders(value) {
} }
export function toMarkdownTable(value, pretty = false, quoteGuids = false) { export function toMarkdownTable(value, pretty = false, quoteGuids = false) {
const headerSpec = arguments[3] ?? { mode: "default" };
const { headers, rows } = getScalarRowsAndHeaders(value); const { headers, rows } = getScalarRowsAndHeaders(value);
const headerDefinitions = headers.map((key, idx) => {
let label = key;
if (headerSpec?.mode === "auto") {
label = toAutoHeaderLabel(key);
} else if (headerSpec?.mode === "list" && Array.isArray(headerSpec.labels) && headerSpec.labels[idx]) {
label = headerSpec.labels[idx];
} else if (headerSpec?.mode === "map" && headerSpec.map && headerSpec.map[key]) {
label = headerSpec.map[key];
}
return { key, label };
});
const renderCell = (raw) => { const renderCell = (raw) => {
const text = formatCell(raw); const text = formatCell(raw);
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text; return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
}; };
if (!pretty) { if (!pretty) {
const headerLine = `| ${headers.join(" | ")} |`; const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;
const separatorLine = `| ${headers.map(() => "---").join(" | ")} |`; const separatorLine = `| ${headerDefinitions.map(() => "---").join(" | ")} |`;
const rowLines = rows.map((row) => const rowLines = rows.map((row) =>
`| ${headers.map((key) => formatCell(row[key])).join(" | ")} |` `| ${headerDefinitions.map((h) => formatCell(row[h.key])).join(" | ")} |`
); );
return [headerLine, separatorLine, ...rowLines].join("\n"); return [headerLine, separatorLine, ...rowLines].join("\n");
} }
const widths = headers.map((header, idx) => const widths = headerDefinitions.map((header, idx) =>
Math.max( Math.max(
header.length, header.label.length,
...rows.map((row) => renderCell(row[headers[idx]]).length), ...rows.map((row) => renderCell(row[headerDefinitions[idx].key]).length),
) )
); );
const renderRow = (values) => const renderRow = (values) =>
`| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`; `| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`;
const headerLine = renderRow(headers); const headerLine = renderRow(headerDefinitions.map((h) => h.label));
const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`; const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`;
const rowLines = rows.map((row) => const rowLines = rows.map((row) =>
renderRow(headers.map((header) => renderCell(row[header]))) renderRow(headerDefinitions.map((header) => renderCell(row[header.key])))
); );
return [headerLine, separatorLine, ...rowLines].join("\n"); return [headerLine, separatorLine, ...rowLines].join("\n");