Refactor CLI commands: remove table command and related utilities; update dependencies and version
All checks were successful
build / build (push) Successful in 17s
All checks were successful
build / build (push) Successful in 17s
This commit is contained in:
@@ -118,19 +118,4 @@ The `sk-az-tools` package may act as a CLI tool that provides various commands f
|
|||||||
|
|
||||||
**Description:** The `list-resource-permissions` command returns available delegated and application permissions exposed by a resource app.
|
**Description:** The `list-resource-permissions` command returns available delegated and application permissions exposed by a resource app.
|
||||||
|
|
||||||
## Table
|
|
||||||
|
|
||||||
**Command name:** `table`
|
|
||||||
|
|
||||||
**Usage:** `sk-az-tools table [--header|-H <definition|auto|a|original|o>] [global options]`
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
|
|
||||||
- `--header`, `-H` <definition|auto|a|original|o> - Header definition. Possible values:
|
|
||||||
- `col1, col2, ...` - Column names separated by comma. The number of columns must match the number of columns in the table.
|
|
||||||
- `key1: Col1, key2: Col2, ...` - property names followed by column and the column name. The number of pairs must match the number of columns in the table.
|
|
||||||
- `auto`, `a` - header is generated automatically based on the first row of the table
|
|
||||||
- `original`, `o` - header is generated based on the original table (before any transformations)
|
|
||||||
|
|
||||||
**Description:** The `table` command act as a filter that transforms JSON input into a Markdown table. It uses built-in Markdown table formatter, but on arbitrary JSON input.
|
|
||||||
|
|
||||||
|
|||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -1,21 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/identity": "^4.13.0",
|
"@azure/identity": "^4.13.0",
|
||||||
"@azure/msal-node": "^5.0.3",
|
"@azure/msal-node": "^5.0.3",
|
||||||
"@azure/msal-node-extensions": "^1.2.0",
|
"@azure/msal-node-extensions": "^1.2.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
"@slawek/sk-tools": ">=0.1.0",
|
||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
"d3-dsv": "^3.0.1",
|
|
||||||
"jmespath": "^0.16.0",
|
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
@@ -23,8 +22,6 @@
|
|||||||
"sk-az-tools": "dist/cli.js"
|
"sk-az-tools": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/d3-dsv": "^3.0.7",
|
|
||||||
"@types/jmespath": "^0.15.2",
|
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
@@ -271,19 +268,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/d3-dsv": {
|
"node_modules/@slawek/sk-tools": {
|
||||||
"version": "3.0.7",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
"resolved": "https://gitea.koszewscy.waw.pl/api/packages/slawek/npm/%40slawek%2Fsk-tools/-/0.1.0/sk-tools-0.1.0.tgz",
|
||||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
"integrity": "sha512-/55dkzgeoh3OWnxnT5jRrbj211YJ73QvZiXjVzfZSg5cdYd8mGumGPxl9Krx+VGaj5b6vTUyFTHafH5+rFewMg==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"license": "MIT"
|
"dependencies": {
|
||||||
},
|
"d3-dsv": "^3.0.1",
|
||||||
"node_modules/@types/jmespath": {
|
"jmespath": "^0.16.0"
|
||||||
"version": "0.15.2",
|
},
|
||||||
"resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz",
|
"bin": {
|
||||||
"integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==",
|
"sk-tools": "dist/cli.js"
|
||||||
"dev": true,
|
}
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.11.0",
|
"version": "24.11.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -23,15 +23,12 @@
|
|||||||
"@azure/msal-node": "^5.0.3",
|
"@azure/msal-node": "^5.0.3",
|
||||||
"@azure/msal-node-extensions": "^1.2.0",
|
"@azure/msal-node-extensions": "^1.2.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
"@slawek/sk-tools": ">=0.1.0",
|
||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
"d3-dsv": "^3.0.1",
|
|
||||||
"jmespath": "^0.16.0",
|
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/d3-dsv": "^3.0.7",
|
|
||||||
"@types/jmespath": "^0.15.2",
|
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
|
|||||||
12
src/cli.ts
12
src/cli.ts
@@ -12,19 +12,19 @@ import { usageListResourcePermissions } from "./cli/commands/list-resource-permi
|
|||||||
import { usageLogin } from "./cli/commands/login.ts";
|
import { usageLogin } from "./cli/commands/login.ts";
|
||||||
import { usageLogout } from "./cli/commands/logout.ts";
|
import { usageLogout } from "./cli/commands/logout.ts";
|
||||||
import { usageRest } from "./cli/commands/rest.ts";
|
import { usageRest } from "./cli/commands/rest.ts";
|
||||||
import { usageTable } from "./cli/commands/table.ts";
|
|
||||||
import {
|
import {
|
||||||
normalizeOutputFormat,
|
normalizeOutputFormat,
|
||||||
omitPermissionGuidColumns,
|
|
||||||
outputFiltered,
|
outputFiltered,
|
||||||
parseHeaderSpec,
|
parseHeaderSpec,
|
||||||
renderOutput,
|
renderOutput,
|
||||||
} from "./cli/utils.ts";
|
} from "@slawek/sk-tools";
|
||||||
|
import {
|
||||||
|
omitPermissionGuidColumns,
|
||||||
|
} from "./cli/permission-utils.ts";
|
||||||
|
|
||||||
type CliValues = {
|
type CliValues = {
|
||||||
help?: boolean;
|
help?: boolean;
|
||||||
type?: string;
|
type?: string;
|
||||||
from?: string;
|
|
||||||
method?: string;
|
method?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
"display-name"?: string;
|
"display-name"?: string;
|
||||||
@@ -56,7 +56,6 @@ Commands:
|
|||||||
list-app-permissions List required permissions for an app
|
list-app-permissions List required permissions for an app
|
||||||
list-app-grants List OAuth2 grants for an app
|
list-app-grants List OAuth2 grants for an app
|
||||||
list-resource-permissions List available permissions for a resource app
|
list-resource-permissions List available permissions for a resource app
|
||||||
table Render stdin JSON as Markdown table
|
|
||||||
|
|
||||||
Global options (all commands):
|
Global options (all commands):
|
||||||
-q, --query <jmespath>
|
-q, --query <jmespath>
|
||||||
@@ -85,8 +84,6 @@ function usageCommand(command: string): string {
|
|||||||
return usageListAppGrants();
|
return usageListAppGrants();
|
||||||
case "list-resource-permissions":
|
case "list-resource-permissions":
|
||||||
return usageListResourcePermissions();
|
return usageListResourcePermissions();
|
||||||
case "table":
|
|
||||||
return usageTable();
|
|
||||||
default:
|
default:
|
||||||
return `Unknown command: ${command}\n\n${usage()}`;
|
return `Unknown command: ${command}\n\n${usage()}`;
|
||||||
}
|
}
|
||||||
@@ -110,7 +107,6 @@ async function main(): Promise<void> {
|
|||||||
options: {
|
options: {
|
||||||
help: { type: "boolean", short: "h" },
|
help: { type: "boolean", short: "h" },
|
||||||
type: { type: "string", short: "t" },
|
type: { type: "string", short: "t" },
|
||||||
from: { type: "string", short: "F" },
|
|
||||||
method: { type: "string" },
|
method: { type: "string" },
|
||||||
url: { type: "string" },
|
url: { type: "string" },
|
||||||
"display-name": { type: "string", short: "n" },
|
"display-name": { type: "string", short: "n" },
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { runListResourcePermissionsCommand } from "./commands/list-resource-perm
|
|||||||
import { runLoginCommand } from "./commands/login.ts";
|
import { runLoginCommand } from "./commands/login.ts";
|
||||||
import { runLogoutCommand } from "./commands/logout.ts";
|
import { runLogoutCommand } from "./commands/logout.ts";
|
||||||
import { runRestCommand } from "./commands/rest.ts";
|
import { runRestCommand } from "./commands/rest.ts";
|
||||||
import { runTableCommand } from "./commands/table.ts";
|
|
||||||
|
|
||||||
import type { CommandValues } from "./commands/types.ts";
|
import type { CommandValues } from "./commands/types.ts";
|
||||||
|
|
||||||
@@ -18,8 +17,6 @@ export async function runCommand(command: string, values: CommandValues): Promis
|
|||||||
return runLoginCommand(values);
|
return runLoginCommand(values);
|
||||||
case "logout":
|
case "logout":
|
||||||
return runLogoutCommand(values);
|
return runLogoutCommand(values);
|
||||||
case "table":
|
|
||||||
return runTableCommand(values);
|
|
||||||
case "list-apps":
|
case "list-apps":
|
||||||
return runListAppsCommand(values);
|
return runListAppsCommand(values);
|
||||||
case "list-app-permissions":
|
case "list-app-permissions":
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { readCsvFromStdin, readJsonFromStdin } from "../utils.ts";
|
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
|
||||||
|
|
||||||
export function usageTable(): string {
|
|
||||||
return `Usage: sk-az-tools table [--from|-F <json|csv|tsv>] [--header|-H <definition|auto|a|original|o>] [global options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--from, -F <json|csv|tsv> Input format on stdin (default: json)
|
|
||||||
--header, -H <value> Header mode: auto|a (default), original|o, or "col1, col2" or "key1: Label 1, key2: Label 2"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runTableCommand(values: CommandValues): Promise<unknown> {
|
|
||||||
const from = (values.from ?? "json").toString().trim().toLowerCase();
|
|
||||||
if (from === "json") {
|
|
||||||
return readJsonFromStdin();
|
|
||||||
}
|
|
||||||
if (from === "csv") {
|
|
||||||
return readCsvFromStdin(",");
|
|
||||||
}
|
|
||||||
if (from === "tsv") {
|
|
||||||
return readCsvFromStdin("\t");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
export type CommandValues = {
|
export type CommandValues = {
|
||||||
[key: string]: string | boolean | undefined;
|
[key: string]: string | boolean | undefined;
|
||||||
type?: string;
|
type?: string;
|
||||||
from?: string;
|
|
||||||
method?: string;
|
method?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
header?: string;
|
header?: string;
|
||||||
|
|||||||
14
src/cli/permission-utils.ts
Normal file
14
src/cli/permission-utils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
243
src/cli/utils.ts
243
src/cli/utils.ts
@@ -1,243 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import jmespath from "jmespath";
|
|
||||||
import { dsvFormat } from "d3-dsv";
|
|
||||||
|
|
||||||
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 async function readCsvFromStdin(separator: string): Promise<unknown> {
|
|
||||||
if (!separator) {
|
|
||||||
throw new Error("separator is required");
|
|
||||||
}
|
|
||||||
if (separator.length !== 1) {
|
|
||||||
throw new Error("separator must be a single character");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 separated values input provided on stdin");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parser = dsvFormat(separator);
|
|
||||||
const rows = parser.parse(input);
|
|
||||||
if (rows.columns.some((header: string) => header.trim() === "")) {
|
|
||||||
throw new Error("header row contains empty column name");
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Invalid separated 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
138
src/markdown.ts
138
src/markdown.ts
@@ -1,138 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
type Scalar = string | number | boolean | null | undefined;
|
|
||||||
type ScalarRow = Record<string, Scalar>;
|
|
||||||
|
|
||||||
type HeaderSpec =
|
|
||||||
| { mode: "default" }
|
|
||||||
| { mode: "auto" }
|
|
||||||
| { mode: "original" }
|
|
||||||
| { mode: "list"; labels: string[] }
|
|
||||||
| { mode: "map"; map: Record<string, string> };
|
|
||||||
|
|
||||||
function formatCell(value: unknown): string {
|
|
||||||
const text = value == null
|
|
||||||
? ""
|
|
||||||
: String(value);
|
|
||||||
return text.replaceAll("|", "\\|").replaceAll("\n", "<br>");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGuid(value: unknown): value is string {
|
|
||||||
return typeof value === "string"
|
|
||||||
&& /^[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: string): string {
|
|
||||||
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 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toMarkdownTable(
|
|
||||||
value: unknown,
|
|
||||||
pretty = false,
|
|
||||||
quoteGuids = false,
|
|
||||||
headerSpec: HeaderSpec = { mode: "default" },
|
|
||||||
): string {
|
|
||||||
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" && headerSpec.labels[idx]) {
|
|
||||||
label = headerSpec.labels[idx];
|
|
||||||
} else if (headerSpec.mode === "map" && headerSpec.map[key]) {
|
|
||||||
label = headerSpec.map[key];
|
|
||||||
}
|
|
||||||
return { key, label };
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderCell = (raw: Scalar): string => {
|
|
||||||
const text = formatCell(raw);
|
|
||||||
return quoteGuids && isGuid(raw) ? `\`${text}\`` : text;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!pretty) {
|
|
||||||
const headerLine = `| ${headerDefinitions.map((h) => h.label).join(" | ")} |`;
|
|
||||||
const separatorLine = `| ${headerDefinitions.map(() => "---").join(" | ")} |`;
|
|
||||||
const rowLines = rows.map((row) =>
|
|
||||||
`| ${headerDefinitions.map((h) => formatCell(row[h.key])).join(" | ")} |`,
|
|
||||||
);
|
|
||||||
return [headerLine, separatorLine, ...rowLines].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
const widths = headerDefinitions.map((header, idx) =>
|
|
||||||
Math.max(
|
|
||||||
header.label.length,
|
|
||||||
...rows.map((row) => renderCell(row[headerDefinitions[idx].key]).length),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderRow = (values: string[]): string =>
|
|
||||||
`| ${values.map((v, idx) => v.padEnd(widths[idx], " ")).join(" | ")} |`;
|
|
||||||
|
|
||||||
const headerLine = renderRow(headerDefinitions.map((h) => h.label));
|
|
||||||
const separatorLine = `|-${widths.map((w) => "-".repeat(w)).join("-|-")}-|`;
|
|
||||||
const rowLines = rows.map((row) =>
|
|
||||||
renderRow(headerDefinitions.map((header) => renderCell(row[header.key]))),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [headerLine, separatorLine, ...rowLines].join("\n");
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user