refactor(cli): split commands and utils; restore cli executable bit

This commit is contained in:
2026-02-08 14:54:08 +01:00
parent eb8a2562a6
commit 3d03600cef
3 changed files with 202 additions and 196 deletions

View File

@@ -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 <command> [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) => {