206 lines
5.6 KiB
TypeScript
206 lines
5.6 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
|
|
import jmespath from "jmespath";
|
|
|
|
import { toMarkdownTable } from "../markdown.ts";
|
|
|
|
type HeaderSpec =
|
|
| { mode: "auto" }
|
|
| { mode: "original" }
|
|
| { mode: "list"; labels: string[] }
|
|
| { mode: "map"; map: Record<string, string> };
|
|
|
|
type OutputFormat = "json" | "table" | "alignedtable" | "prettytable" | "tsv";
|
|
|
|
type Scalar = string | number | boolean | null | undefined;
|
|
type ScalarRow = Record<string, Scalar>;
|
|
|
|
export function outputFiltered(object: unknown, query?: string): unknown {
|
|
return query
|
|
? jmespath.search(object, query)
|
|
: object;
|
|
}
|
|
|
|
export function parseHeaderSpec(headerValue?: string): HeaderSpec {
|
|
if (!headerValue) {
|
|
return { mode: "auto" };
|
|
}
|
|
|
|
const raw = headerValue.trim();
|
|
if (raw === "" || raw.toLowerCase() === "auto" || raw.toLowerCase() === "a") {
|
|
return { mode: "auto" };
|
|
}
|
|
if (raw.toLowerCase() === "original" || raw.toLowerCase() === "o") {
|
|
return { mode: "original" };
|
|
}
|
|
|
|
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: Record<string, string> = {};
|
|
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 };
|
|
}
|
|
|
|
export function normalizeOutputFormat(outputValue?: string): OutputFormat {
|
|
if (outputValue == null) {
|
|
return "json";
|
|
}
|
|
|
|
const raw = outputValue.toLowerCase();
|
|
if (raw === "json") {
|
|
throw new Error("JSON is the default output. Omit --output to use it.");
|
|
}
|
|
if (raw === "j") {
|
|
throw new Error("JSON is the default output. Omit --output to use it.");
|
|
}
|
|
if (raw === "table" || raw === "t") return "table";
|
|
if (raw === "alignedtable" || raw === "at") return "alignedtable";
|
|
if (raw === "prettytable" || raw === "pt") return "prettytable";
|
|
if (raw === "tsv") return "tsv";
|
|
throw new Error("--output must be one of: table|t, alignedtable|at, prettytable|pt, tsv");
|
|
}
|
|
|
|
function isScalar(value: unknown): value is Scalar {
|
|
return value == null || typeof value !== "object";
|
|
}
|
|
|
|
function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } {
|
|
let rows: Array<Record<string, unknown>>;
|
|
if (Array.isArray(value)) {
|
|
rows = value.map((item) =>
|
|
item && typeof item === "object" && !Array.isArray(item)
|
|
? item as Record<string, unknown>
|
|
: { value: item },
|
|
);
|
|
} else if (value && typeof value === "object") {
|
|
rows = [value as Record<string, unknown>];
|
|
} else {
|
|
rows = [{ value }];
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
return {
|
|
headers: ["result"],
|
|
rows: [{ result: "" }],
|
|
};
|
|
}
|
|
|
|
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))]
|
|
.filter((key) =>
|
|
rows.every((row) => isScalar(row[key])),
|
|
);
|
|
|
|
if (headers.length === 0) {
|
|
return {
|
|
headers: ["result"],
|
|
rows: [{ result: "" }],
|
|
};
|
|
}
|
|
|
|
const scalarRows: ScalarRow[] = rows.map((row) => {
|
|
const result: ScalarRow = {};
|
|
for (const [key, rowValue] of Object.entries(row)) {
|
|
if (isScalar(rowValue)) {
|
|
result[key] = rowValue;
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
|
|
return { headers, rows: scalarRows };
|
|
}
|
|
|
|
function toTsv(value: unknown): string {
|
|
const { headers, rows } = getScalarRowsAndHeaders(value);
|
|
const lines = rows.map((row) =>
|
|
headers
|
|
.map((header) => (row[header] == null ? "" : String(row[header]).replaceAll("\t", " ").replaceAll("\n", " ")))
|
|
.join("\t"),
|
|
);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function omitPermissionGuidColumns(value: unknown): unknown {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => omitPermissionGuidColumns(item));
|
|
}
|
|
if (!value || typeof value !== "object") {
|
|
return value;
|
|
}
|
|
const { resourceAppId, permissionId, ...rest } = value as Record<string, unknown>;
|
|
void resourceAppId;
|
|
void permissionId;
|
|
return rest;
|
|
}
|
|
|
|
export async function readJsonFromStdin(): Promise<unknown> {
|
|
const input = await new Promise<string>((resolve, reject) => {
|
|
let data = "";
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.on("data", (chunk: string) => {
|
|
data += chunk;
|
|
});
|
|
process.stdin.on("end", () => {
|
|
resolve(data);
|
|
});
|
|
process.stdin.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
if (!input.trim()) {
|
|
throw new Error("No JSON input provided on stdin");
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(input) as unknown;
|
|
} catch (err) {
|
|
throw new Error(`Invalid JSON input on stdin: ${(err as Error).message}`);
|
|
}
|
|
}
|
|
|
|
export function renderOutput(
|
|
command: string,
|
|
output: unknown,
|
|
outputFormat: OutputFormat,
|
|
headerSpec: HeaderSpec,
|
|
): void {
|
|
if (outputFormat === "tsv") {
|
|
console.log(toTsv(output));
|
|
return;
|
|
}
|
|
|
|
if (command === "table") {
|
|
console.log(toMarkdownTable(
|
|
output,
|
|
outputFormat === "alignedtable" || outputFormat === "prettytable",
|
|
outputFormat === "prettytable",
|
|
headerSpec,
|
|
));
|
|
} else if (outputFormat === "alignedtable") {
|
|
console.log(toMarkdownTable(output, true, false, headerSpec));
|
|
} else if (outputFormat === "prettytable") {
|
|
console.log(toMarkdownTable(output, true, true, headerSpec));
|
|
} else if (outputFormat === "table") {
|
|
console.log(toMarkdownTable(output, false, false, headerSpec));
|
|
} else {
|
|
console.log(JSON.stringify(output, null, 2));
|
|
}
|
|
}
|