feat(cli): add stdin table command and pretty rendering option

This commit is contained in:
2026-02-08 11:52:51 +01:00
parent a22b762180
commit 6c5b628c70
2 changed files with 61 additions and 13 deletions

View File

@@ -15,11 +15,13 @@ Commands:
list-apps [--display-name <name>] list-apps [--display-name <name>]
list-app-permissions --app-id <appId> list-app-permissions --app-id <appId>
list-app-grants --app-id <appId> list-app-grants --app-id <appId>
table [--pretty]
Options: Options:
--display-name <name> Filter apps by exact display name --display-name <name> Filter apps by exact display name
--app-id <appId> Application (client) ID --app-id <appId> Application (client) ID
--query <jmespath> Filter output JSON using JMESPath --query <jmespath> Filter output JSON using JMESPath
--pretty Use normalized column widths for Markdown table output
--output <format> Output format: json|table|prettytable (default: json) --output <format> Output format: json|table|prettytable (default: json)
-h, --help Show this help message`; -h, --help Show this help message`;
} }
@@ -30,6 +32,31 @@ function outputFiltered(object, query) {
: object; : object;
} }
async function readJsonFromStdin() {
const input = await new Promise((resolve, reject) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
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);
} catch (err) {
throw new Error(`Invalid JSON input on stdin: ${err.message}`);
}
}
async function main() { async function main() {
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const command = argv[0]; const command = argv[0];
@@ -45,6 +72,7 @@ async function main() {
"display-name": { type: "string" }, "display-name": { type: "string" },
"app-id": { type: "string" }, "app-id": { type: "string" },
query: { type: "string" }, query: { type: "string" },
pretty: { type: "boolean" },
output: { type: "string" }, output: { type: "string" },
}, },
strict: true, strict: true,
@@ -60,37 +88,57 @@ async function main() {
throw new Error("--output must be one of: json, table, prettytable"); throw new Error("--output must be one of: json, table, prettytable");
} }
let result;
switch (command) {
case "table":
result = await readJsonFromStdin();
break;
case "list-apps":
{
const config = await loadPublicConfig(); const config = await loadPublicConfig();
const { client } = await getGraphClient({ const { client } = await getGraphClient({
tenantId: config.tenantId, tenantId: config.tenantId,
clientId: config.clientId, clientId: config.clientId,
}); });
let result;
switch (command) {
case "list-apps":
result = await listApps(client, { result = await listApps(client, {
displayName: values["display-name"], displayName: values["display-name"],
}); });
}
break; break;
case "list-app-permissions": case "list-app-permissions":
if (!values["app-id"]) { if (!values["app-id"]) {
throw new Error("--app-id is required for list-app-permissions"); throw new Error("--app-id is required for list-app-permissions");
} }
{
const config = await loadPublicConfig();
const { client } = await getGraphClient({
tenantId: config.tenantId,
clientId: config.clientId,
});
result = await listAppPermissions(client, values["app-id"]); result = await listAppPermissions(client, values["app-id"]);
}
break; break;
case "list-app-grants": case "list-app-grants":
if (!values["app-id"]) { if (!values["app-id"]) {
throw new Error("--app-id is required for list-app-grants"); throw new Error("--app-id is required for list-app-grants");
} }
{
const config = await loadPublicConfig();
const { client } = await getGraphClient({
tenantId: config.tenantId,
clientId: config.clientId,
});
result = await listAppGrants(client, values["app-id"]); result = await listAppGrants(client, values["app-id"]);
}
break; break;
default: default:
throw new Error(`Unknown command: ${command}`); throw new Error(`Unknown command: ${command}`);
} }
const filtered = outputFiltered(result, values.query); const filtered = outputFiltered(result, values.query);
if (outputFormat === "prettytable") { if (command === "table") {
console.log(toMarkdownTable(filtered, Boolean(values.pretty)));
} else if (outputFormat === "prettytable") {
console.log(toMarkdownTable(filtered, true)); console.log(toMarkdownTable(filtered, true));
} else if (outputFormat === "table") { } else if (outputFormat === "table") {
console.log(toMarkdownTable(filtered)); console.log(toMarkdownTable(filtered));

View File

@@ -8,7 +8,7 @@ import { Client } from "@microsoft/microsoft-graph-client";
* @param { Object } options - Options for authentication * @param { Object } options - Options for authentication
* @param { string } options.tenantId - The Azure AD tenant ID * @param { string } options.tenantId - The Azure AD tenant ID
* @param { string } options.clientId - The Azure AD client ID * @param { string } options.clientId - The Azure AD client ID
* @returns { Object } An object containing the Graph API token and client * @returns { Promise<{ graphApiToken: Object, client: Object }> } An object containing the Graph API token and client
*/ */
export async function getGraphClient({ tenantId, clientId }) { export async function getGraphClient({ tenantId, clientId }) {
const graphApiToken = await loginInteractive({ const graphApiToken = await loginInteractive({