From 3d03600cef59765bdcf30be284a155833defd5be Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sun, 8 Feb 2026 14:54:08 +0100 Subject: [PATCH] refactor(cli): split commands and utils; restore cli executable bit --- src/cli.js | 207 +++----------------------------------------- src/cli/commands.js | 82 ++++++++++++++++++ src/cli/utils.js | 109 +++++++++++++++++++++++ 3 files changed, 202 insertions(+), 196 deletions(-) create mode 100644 src/cli/commands.js create mode 100644 src/cli/utils.js diff --git a/src/cli.js b/src/cli.js index 8b29f6e..0966449 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,21 +1,16 @@ #!/usr/bin/env node // SPDX-License-Identifier: MIT - import { parseArgs } from "node:util"; -import jmespath from "jmespath"; -import { minimatch } from "minimatch"; -import { loadPublicConfig } from "./index.js"; -import { getGraphClient } from "./graph/auth.js"; +import { runCommand } from "./cli/commands.js"; import { - listApps, - listAppPermissions, - listAppPermissionsResolved, - listAppGrants, - listResourcePermissions, -} from "./graph/app.js"; -import { toMarkdownTable } from "./markdown.js"; + normalizeOutputFormat, + omitPermissionGuidColumns, + outputFiltered, + parseHeaderSpec, + renderOutput, +} from "./cli/utils.js"; function usage() { return `Usage: sk-az-tools [options] @@ -39,91 +34,6 @@ Options: -h, --help Show this help message`; } -function outputFiltered(object, query) { - return query - ? jmespath.search(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 normalizeOutputFormat(outputValue) { - const raw = (outputValue ?? "json").toLowerCase(); - if (raw === "json" || raw === "j") return "json"; - if (raw === "table" || raw === "t") return "table"; - if (raw === "alignedtable" || raw === "at") return "alignedtable"; - if (raw === "prettytable" || raw === "pt") return "prettytable"; - throw new Error("--output must be one of: json|j, table|t, alignedtable|at, prettytable|pt"); -} - -function omitPermissionGuidColumns(value) { - if (Array.isArray(value)) { - return value.map((item) => omitPermissionGuidColumns(item)); - } - if (!value || typeof value !== "object") { - return value; - } - const { resourceAppId, permissionId, ...rest } = value; - return rest; -} - -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() { const argv = process.argv.slice(2); const command = argv[0]; @@ -153,111 +63,16 @@ async function main() { console.log(usage()); process.exit(0); } + const outputFormat = normalizeOutputFormat(values.output); - - let result; - switch (command) { - case "table": - result = await readJsonFromStdin(); - break; - case "list-apps": - { - const config = await loadPublicConfig(); - const { client } = await getGraphClient({ - tenantId: config.tenantId, - clientId: config.clientId, - }); - result = await listApps(client, { - displayName: values["display-name"], - }); - } - break; - case "list-app-permissions": - if (!values["app-id"]) { - 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 = values.resolve || values.filter - ? await listAppPermissionsResolved(client, values["app-id"]) - : await listAppPermissions(client, values["app-id"]); - if (values.filter) { - const pattern = values.filter; - result = result.filter((item) => - minimatch(item.permissionValue ?? "", pattern, { nocase: true }) - || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }) - ); - } - } - break; - case "list-app-grants": - if (!values["app-id"]) { - 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"]); - } - break; - case "list-resource-permissions": - if (!values["app-id"] && !values["display-name"]) { - throw new Error("--app-id or --display-name is required for list-resource-permissions"); - } - if (values["app-id"] && values["display-name"]) { - throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both"); - } - { - const config = await loadPublicConfig(); - const { client } = await getGraphClient({ - tenantId: config.tenantId, - clientId: config.clientId, - }); - result = await listResourcePermissions(client, { - appId: values["app-id"], - displayName: values["display-name"], - }); - if (values.filter) { - const pattern = values.filter; - result = result.filter((item) => - minimatch(item.permissionValue ?? "", pattern, { nocase: true }) - || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }) - ); - } - } - break; - default: - throw new Error(`Unknown command: ${command}`); - } - + const result = await runCommand(command, values); const filtered = outputFiltered(result, values.query); const output = command === "list-app-permissions" && values.short ? omitPermissionGuidColumns(filtered) : filtered; const headerSpec = parseHeaderSpec(values.header); - 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)); - } + + renderOutput(command, output, outputFormat, headerSpec); } main().catch((err) => { diff --git a/src/cli/commands.js b/src/cli/commands.js new file mode 100644 index 0000000..1d942f1 --- /dev/null +++ b/src/cli/commands.js @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +import { minimatch } from "minimatch"; + +import { loadPublicConfig } from "../index.js"; +import { getGraphClient } from "../graph/auth.js"; +import { + listApps, + listAppPermissions, + listAppPermissionsResolved, + listAppGrants, + listResourcePermissions, +} from "../graph/app.js"; +import { readJsonFromStdin } from "./utils.js"; + +function filterByPermissionName(rows, pattern) { + return rows.filter((item) => + minimatch(item.permissionValue ?? "", pattern, { nocase: true }) + || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }) + ); +} + +async function getGraphClientFromPublicConfig() { + const config = await loadPublicConfig(); + return getGraphClient({ + tenantId: config.tenantId, + clientId: config.clientId, + }); +} + +export async function runCommand(command, values) { + switch (command) { + case "table": + return readJsonFromStdin(); + case "list-apps": { + const { client } = await getGraphClientFromPublicConfig(); + return listApps(client, { + displayName: values["display-name"], + }); + } + case "list-app-permissions": { + if (!values["app-id"]) { + throw new Error("--app-id is required for list-app-permissions"); + } + const { client } = await getGraphClientFromPublicConfig(); + let result = values.resolve || values.filter + ? await listAppPermissionsResolved(client, values["app-id"]) + : await listAppPermissions(client, values["app-id"]); + if (values.filter) { + result = filterByPermissionName(result, values.filter); + } + return result; + } + case "list-app-grants": { + if (!values["app-id"]) { + throw new Error("--app-id is required for list-app-grants"); + } + const { client } = await getGraphClientFromPublicConfig(); + return listAppGrants(client, values["app-id"]); + } + case "list-resource-permissions": { + if (!values["app-id"] && !values["display-name"]) { + throw new Error("--app-id or --display-name is required for list-resource-permissions"); + } + if (values["app-id"] && values["display-name"]) { + throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both"); + } + + const { client } = await getGraphClientFromPublicConfig(); + let result = await listResourcePermissions(client, { + appId: values["app-id"], + displayName: values["display-name"], + }); + if (values.filter) { + result = filterByPermissionName(result, values.filter); + } + return result; + } + default: + throw new Error(`Unknown command: ${command}`); + } +} diff --git a/src/cli/utils.js b/src/cli/utils.js new file mode 100644 index 0000000..224ef17 --- /dev/null +++ b/src/cli/utils.js @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT + +import jmespath from "jmespath"; + +import { toMarkdownTable } from "../markdown.js"; + +export function outputFiltered(object, query) { + return query + ? jmespath.search(object, query) + : object; +} + +export 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 }; +} + +export function normalizeOutputFormat(outputValue) { + const raw = (outputValue ?? "json").toLowerCase(); + if (raw === "json" || raw === "j") return "json"; + if (raw === "table" || raw === "t") return "table"; + if (raw === "alignedtable" || raw === "at") return "alignedtable"; + if (raw === "prettytable" || raw === "pt") return "prettytable"; + throw new Error("--output must be one of: json|j, table|t, alignedtable|at, prettytable|pt"); +} + +export function omitPermissionGuidColumns(value) { + if (Array.isArray(value)) { + return value.map((item) => omitPermissionGuidColumns(item)); + } + if (!value || typeof value !== "object") { + return value; + } + const { resourceAppId, permissionId, ...rest } = value; + return rest; +} + +export 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}`); + } +} + +export function renderOutput(command, output, outputFormat, headerSpec) { + 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)); + } +}