refactor(cli): split commands and utils; restore cli executable bit
This commit is contained in:
207
src/cli.js
207
src/cli.js
@@ -1,21 +1,16 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
import jmespath from "jmespath";
|
|
||||||
import { minimatch } from "minimatch";
|
|
||||||
|
|
||||||
import { loadPublicConfig } from "./index.js";
|
import { runCommand } from "./cli/commands.js";
|
||||||
import { getGraphClient } from "./graph/auth.js";
|
|
||||||
import {
|
import {
|
||||||
listApps,
|
normalizeOutputFormat,
|
||||||
listAppPermissions,
|
omitPermissionGuidColumns,
|
||||||
listAppPermissionsResolved,
|
outputFiltered,
|
||||||
listAppGrants,
|
parseHeaderSpec,
|
||||||
listResourcePermissions,
|
renderOutput,
|
||||||
} from "./graph/app.js";
|
} from "./cli/utils.js";
|
||||||
import { toMarkdownTable } from "./markdown.js";
|
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
return `Usage: sk-az-tools <command> [options]
|
return `Usage: sk-az-tools <command> [options]
|
||||||
@@ -39,91 +34,6 @@ Options:
|
|||||||
-h, --help Show this help message`;
|
-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() {
|
async function main() {
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
const command = argv[0];
|
const command = argv[0];
|
||||||
@@ -153,111 +63,16 @@ async function main() {
|
|||||||
console.log(usage());
|
console.log(usage());
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputFormat = normalizeOutputFormat(values.output);
|
const outputFormat = normalizeOutputFormat(values.output);
|
||||||
|
const result = await runCommand(command, values);
|
||||||
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 filtered = outputFiltered(result, values.query);
|
const filtered = outputFiltered(result, values.query);
|
||||||
const output = command === "list-app-permissions" && values.short
|
const output = command === "list-app-permissions" && values.short
|
||||||
? omitPermissionGuidColumns(filtered)
|
? omitPermissionGuidColumns(filtered)
|
||||||
: filtered;
|
: filtered;
|
||||||
const headerSpec = parseHeaderSpec(values.header);
|
const headerSpec = parseHeaderSpec(values.header);
|
||||||
if (command === "table") {
|
|
||||||
console.log(toMarkdownTable(
|
renderOutput(command, output, outputFormat, headerSpec);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|||||||
82
src/cli/commands.js
Normal file
82
src/cli/commands.js
Normal file
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/cli/utils.js
Normal file
109
src/cli/utils.js
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user