feat(table): add header spec support and explicit auto mode
This commit is contained in:
55
src/cli.js
55
src/cli.js
@@ -22,7 +22,7 @@ Commands:
|
||||
list-apps [--display-name|-n <name>]
|
||||
list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s]
|
||||
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:
|
||||
-n, --display-name <name> Filter apps by exact display name
|
||||
@@ -32,6 +32,7 @@ Options:
|
||||
-q, --query <jmespath> Filter output JSON using JMESPath
|
||||
-p, --pretty Use normalized column widths for Markdown table output
|
||||
-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)
|
||||
-h, --help Show this help message`;
|
||||
}
|
||||
@@ -42,6 +43,40 @@ function outputFiltered(object, query) {
|
||||
: 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) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => omitPermissionGuidColumns(item));
|
||||
@@ -97,6 +132,7 @@ async function main() {
|
||||
query: { type: "string", short: "q" },
|
||||
pretty: { type: "boolean", short: "p" },
|
||||
"quote-guids": { type: "boolean", short: "g" },
|
||||
header: { type: "string", short: "H" },
|
||||
output: { type: "string", short: "o" },
|
||||
},
|
||||
strict: true,
|
||||
@@ -165,12 +201,23 @@ async function main() {
|
||||
const output = command === "list-app-permissions" && values.short
|
||||
? omitPermissionGuidColumns(filtered)
|
||||
: filtered;
|
||||
const headerSpec = parseHeaderSpec(values.header);
|
||||
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") {
|
||||
console.log(toMarkdownTable(output, true, Boolean(values["quote-guids"])));
|
||||
console.log(toMarkdownTable(
|
||||
output,
|
||||
true,
|
||||
Boolean(values["quote-guids"]),
|
||||
headerSpec,
|
||||
));
|
||||
} else if (outputFormat === "table") {
|
||||
console.log(toMarkdownTable(output));
|
||||
console.log(toMarkdownTable(output, false, false, headerSpec));
|
||||
} else {
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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) {
|
||||
let rows;
|
||||
if (Array.isArray(value)) {
|
||||
@@ -52,35 +65,48 @@ function getScalarRowsAndHeaders(value) {
|
||||
}
|
||||
|
||||
export function toMarkdownTable(value, pretty = false, quoteGuids = false) {
|
||||
const headerSpec = arguments[3] ?? { mode: "default" };
|
||||
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 text = formatCell(raw);
|
||||
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
|
||||
};
|
||||
|
||||
if (!pretty) {
|
||||
const headerLine = `| ${headers.join(" | ")} |`;
|
||||
const separatorLine = `| ${headers.map(() => "---").join(" | ")} |`;
|
||||
const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;
|
||||
const separatorLine = `| ${headerDefinitions.map(() => "---").join(" | ")} |`;
|
||||
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");
|
||||
}
|
||||
|
||||
const widths = headers.map((header, idx) =>
|
||||
const widths = headerDefinitions.map((header, idx) =>
|
||||
Math.max(
|
||||
header.length,
|
||||
...rows.map((row) => renderCell(row[headers[idx]]).length),
|
||||
header.label.length,
|
||||
...rows.map((row) => renderCell(row[headerDefinitions[idx].key]).length),
|
||||
)
|
||||
);
|
||||
|
||||
const renderRow = (values) =>
|
||||
`| ${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 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");
|
||||
|
||||
Reference in New Issue
Block a user