diff --git a/package-lock.json b/package-lock.json index 0a6ecf3..682bf01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@slawek/sk-az-tools", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@slawek/sk-az-tools", - "version": "0.3.1", + "version": "0.3.2", "license": "MIT", "dependencies": { "@azure/identity": "^4.13.0", @@ -14,6 +14,7 @@ "@azure/msal-node-extensions": "^1.2.0", "@microsoft/microsoft-graph-client": "^3.0.7", "azure-devops-node-api": "^15.1.2", + "d3-dsv": "^3.0.1", "jmespath": "^0.16.0", "minimatch": "^10.1.2", "open": "^10.1.0" @@ -22,6 +23,7 @@ "sk-az-tools": "dist/cli.js" }, "devDependencies": { + "@types/d3-dsv": "^3.0.7", "@types/jmespath": "^0.15.2", "@types/node": "^24.0.0", "typescript": "^5.8.2" @@ -269,6 +271,13 @@ } } }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jmespath": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", @@ -454,6 +463,40 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -745,6 +788,18 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1155,6 +1210,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1175,6 +1236,12 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", diff --git a/package.json b/package.json index 6c5172a..344b882 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slawek/sk-az-tools", - "version": "0.3.1", + "version": "0.3.2", "type": "module", "files": [ "dist", @@ -24,11 +24,13 @@ "@azure/msal-node-extensions": "^1.2.0", "@microsoft/microsoft-graph-client": "^3.0.7", "azure-devops-node-api": "^15.1.2", + "d3-dsv": "^3.0.1", "jmespath": "^0.16.0", "minimatch": "^10.1.2", "open": "^10.1.0" }, "devDependencies": { + "@types/d3-dsv": "^3.0.7", "@types/jmespath": "^0.15.2", "@types/node": "^24.0.0", "typescript": "^5.8.2" diff --git a/src/cli.ts b/src/cli.ts index ee96cc5..e2f6c3b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,6 +24,7 @@ import { type CliValues = { help?: boolean; type?: string; + from?: string; method?: string; url?: string; "display-name"?: string; @@ -109,6 +110,7 @@ async function main(): Promise { options: { help: { type: "boolean", short: "h" }, type: { type: "string", short: "t" }, + from: { type: "string", short: "F" }, method: { type: "string" }, url: { type: "string" }, "display-name": { type: "string", short: "n" }, diff --git a/src/cli/commands.ts b/src/cli/commands.ts index c6e9b3f..3afe738 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -19,7 +19,7 @@ export async function runCommand(command: string, values: CommandValues): Promis case "logout": return runLogoutCommand(values); case "table": - return runTableCommand(); + return runTableCommand(values); case "list-apps": return runListAppsCommand(values); case "list-app-permissions": diff --git a/src/cli/commands/table.ts b/src/cli/commands/table.ts index 2453159..80ae31c 100644 --- a/src/cli/commands/table.ts +++ b/src/cli/commands/table.ts @@ -1,14 +1,28 @@ // SPDX-License-Identifier: MIT -import { readJsonFromStdin } from "../utils.ts"; +import { readCsvFromStdin, readJsonFromStdin } from "../utils.ts"; + +import type { CommandValues } from "./types.ts"; export function usageTable(): string { - return `Usage: sk-az-tools table [--header|-H ] [global options] + return `Usage: sk-az-tools table [--from|-F ] [--header|-H ] [global options] Options: - -H, --header Header mode: auto|a (default), original|o, or "col1, col2" or "key1: Label 1, key2: Label 2"`; + --from, -F Input format on stdin (default: json) + --header, -H Header mode: auto|a (default), original|o, or "col1, col2" or "key1: Label 1, key2: Label 2"`; } -export async function runTableCommand(): Promise { - return readJsonFromStdin(); +export async function runTableCommand(values: CommandValues): Promise { + 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`); } diff --git a/src/cli/commands/types.ts b/src/cli/commands/types.ts index 6dabd1e..f1ff9c5 100644 --- a/src/cli/commands/types.ts +++ b/src/cli/commands/types.ts @@ -3,6 +3,7 @@ export type CommandValues = { [key: string]: string | boolean | undefined; type?: string; + from?: string; method?: string; url?: string; header?: string; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 6aa5f98..a07f201 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT import jmespath from "jmespath"; +import { dsvFormat } from "d3-dsv"; import { toMarkdownTable } from "../markdown.ts"; @@ -175,6 +176,43 @@ export async function readJsonFromStdin(): Promise { } } +export async function readCsvFromStdin(separator: string): Promise { + 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((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,