diff --git a/package-lock.json b/package-lock.json index 3c7408f..752ae70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@slawek/sk-az-tools", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@slawek/sk-az-tools", - "version": "0.5.1", + "version": "0.6.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.13.0", @@ -15,6 +15,7 @@ "@microsoft/microsoft-graph-client": "^3.0.7", "@slawek/sk-tools": ">=0.2.0", "azure-devops-node-api": "^15.1.2", + "commander": "^14.0.3", "minimatch": "^10.1.2", "open": "^10.1.0", "semver": "^7.7.2", @@ -502,12 +503,12 @@ "license": "MIT" }, "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==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=20" } }, "node_modules/d3-dsv": { @@ -535,6 +536,15 @@ "node": ">=12" } }, + "node_modules/d3-dsv/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/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 09e07ec..4874ea9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slawek/sk-az-tools", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "files": [ "dist", @@ -9,6 +9,7 @@ ], "scripts": { "build": "rm -rf dist && tsc && chmod +x dist/cli.js", + "build:watch": "tsc --watch", "create-pca": "node dist/create-pca.js", "bump-patch": "node scripts/bump-patch.mjs", "make-deps": "node scripts/make-mermaid-func-deps.mjs", @@ -25,6 +26,7 @@ "@microsoft/microsoft-graph-client": "^3.0.7", "@slawek/sk-tools": ">=0.2.0", "azure-devops-node-api": "^15.1.2", + "commander": "^14.0.3", "minimatch": "^10.1.2", "open": "^10.1.0", "semver": "^7.7.2", diff --git a/src/cli.ts b/src/cli.ts index 4cf99c9..9f87539 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,144 +1,133 @@ #!/usr/bin/env node // SPDX-License-Identifier: MIT -import { parseArgs } from "node:util"; +import { Command, Option } from "commander"; +import { renderCliOutput} from "@slawek/sk-tools"; -import { runCommand } from "./cli/commands.ts"; -import { usageGetToken } from "./cli/commands/get-token.ts"; -import { usageListAppGrants } from "./cli/commands/list-app-grants.ts"; -import { usageListAppPermissions } from "./cli/commands/list-app-permissions.ts"; -import { usageListApps } from "./cli/commands/list-apps.ts"; -import { usageListResourcePermissions } from "./cli/commands/list-resource-permissions.ts"; -import { usageLogin } from "./cli/commands/login.ts"; -import { usageLogout } from "./cli/commands/logout.ts"; -import { usageRest } from "./cli/commands/rest.ts"; -import { - renderCliOutput, -} from "@slawek/sk-tools"; +// Commands +import { runGetTokenCommand } from "./cli/commands/get-token.ts"; +import { runListAppGrantsCommand } from "./cli/commands/list-app-grants.ts"; +import { runListAppPermissionsCommand } from "./cli/commands/list-app-permissions.ts"; +import { runListAppsCommand } from "./cli/commands/list-apps.ts"; +import { runListResourcePermissionsCommand } from "./cli/commands/list-resource-permissions.ts"; +import { runLoginCommand } from "./cli/commands/login.ts"; +import { runLogoutCommand } from "./cli/commands/logout.ts"; +import { runRestCommand } from "./cli/commands/rest.ts"; -type CliValues = { - help?: boolean; - type?: string; - method?: string; - url?: string; - "display-name"?: string; - "app-id"?: string; - resources?: string; - "use-device-code"?: boolean; - "no-browser"?: boolean; - browser?: string; - "browser-profile"?: string; - all?: boolean; - resolve?: boolean; - short?: boolean; - filter?: string; - query?: string; - columns?: string; - header?: string; - output?: string; - [key: string]: string | boolean | undefined; -}; - -function usage(): string { - return `Usage: sk-az-tools [options] - -Commands: - login Authenticate selected resources - logout Sign out and clear login state - get-token Get access token (azurerm|devops) - rest Call REST API endpoint - list-apps List Entra applications - list-app-permissions List required permissions for an app - list-app-grants List OAuth2 grants for an app - list-resource-permissions List available permissions for a resource app - -Global options (all commands): - -q, --query - -C, --columns Column tokens: col (raw), col: (auto), col:Label (custom), exact via = prefix - -o, --output table|t|alignedtable|at|prettytable|pt|tsv - -h, --help - -Use: sk-az-tools --help -or: sk-az-tools --help`; -} - -function usageCommand(command: string): string { - switch (command) { - case "login": - return usageLogin(); - case "list-apps": - return usageListApps(); - case "logout": - return usageLogout(); - case "get-token": - return usageGetToken(); - case "rest": - return usageRest(); - case "list-app-permissions": - return usageListAppPermissions(); - case "list-app-grants": - return usageListAppGrants(); - case "list-resource-permissions": - return usageListResourcePermissions(); - default: - return `Unknown command: ${command}\n\n${usage()}`; - } -} +import pkg from "../package.json" with { type: "json" }; +const { version: packageVersion } = pkg; async function main(): Promise { - const argv = process.argv.slice(2); - const command = argv[0]; - if (!command) { - console.log(usage()); - process.exit(0); - } - if (command === "-h" || command === "--help") { - const helpCommand = argv[1]; - console.log(helpCommand ? usageCommand(helpCommand) : usage()); - process.exit(0); - } + const skAzTools = new Command(); - const { values } = parseArgs({ - args: argv.slice(1), - options: { - help: { type: "boolean", short: "h" }, - type: { type: "string", short: "t" }, - method: { type: "string" }, - url: { type: "string" }, - "display-name": { type: "string", short: "n" }, - "app-id": { type: "string", short: "i" }, - resources: { type: "string" }, - "use-device-code": { type: "boolean" }, - "no-browser": { type: "boolean" }, - browser: { type: "string" }, - "browser-profile": { type: "string" }, - all: { type: "boolean" }, - resolve: { type: "boolean", short: "r" }, - short: { type: "boolean", short: "s" }, - filter: { type: "string", short: "f" }, - query: { type: "string", short: "q" }, - columns: { type: "string", short: "C" }, - header: { type: "string" }, - output: { type: "string", short: "o" }, - }, - strict: true, - allowPositionals: false, - }); + skAzTools + .name("sk-az-tools") + .description("A collection of tools for Azure and Microsoft Entra management") + .version(packageVersion) + .option("-q, --query ", "JMESPath query to filter output") + .option("-C, --columns ", "Column tokens: col (raw), col: (auto), col:Label (custom), exact via = prefix") + .addOption(new Option("-o, --output ", "Output format: table|t|alignedtable|at|prettytable|pt|tsv") + .choices(["table", "t", "alignedtable", "at", "prettytable", "pt", "tsv"]) + ); - const typedValues = values as CliValues; + skAzTools + .command("login") + .description("Authenticate selected resources") + .option("--resources ", "Comma-separated resources: graph,devops,arm") + .option("--use-device-code", "Use device code flow") + .option("--no-browser", "Do not launch browser") + .option("--browser ", "Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox") + .option("--browser-profile ", "Chromium profile name") + .action(async (options) => { + const output = await runLoginCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); - if (typedValues.help) { - console.log(usageCommand(command)); - process.exit(0); - } + skAzTools + .command("logout") + .description("Sign out and clear login state") + .option("--all", "Clear login state and remove all cached accounts") + .action(async (options) => { + const output = await runLogoutCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); - const output = await runCommand(command, typedValues); - renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns); + skAzTools + .command("get-token") + .description("Get access token (azurerm|devops)") + .addOption(new Option("-t, --type ", "Token type").choices(["azurerm", "devops"])) + .action(async (options) => { + const output = await runGetTokenCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + skAzTools + .command("rest") + .description("Call REST API endpoint") + .argument("", "Full URL to call") + .addOption(new Option("-X, --method ", "HTTP method") + .choices(["GET", "POST", "PUT", "PATCH", "DELETE"])) + .option("-H, --header ", "Extra request header") + .addHelpText("after", ` +Authorization is added automatically for: + management.azure.com Uses azurerm token + dev.azure.com Uses devops token`) + .action(async (url, options) => { + const output = await runRestCommand(url, options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + skAzTools + .command("list-apps") + .description("List Entra applications") + .option("-n, --display-name ", "Get app by display name") + .option("-i, --app-id ", "Get app by id") + .option("-f, --filter ", "Filter display name glob") + .action(async (options) => { + const output = await runListAppsCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + skAzTools + .command("list-app-permissions") + .description("List required permissions for an app") + .option("-i, --app-id ", "Application (client) ID") + .option("-r, --resolve", "Resolve permission GUIDs to human-readable values") + .option("-s, --short", "Makes output more compact") + .option("-f, --filter ", "Filter by permission name glob") + .action(async (options) => { + const output = await runListAppPermissionsCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + skAzTools + .command("list-app-grants") + .description("List OAuth2 grants for an app") + .option("-i, --app-id ", "Application (client) ID") + .action(async (options) => { + const output = await runListAppGrantsCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + skAzTools + .command("list-resource-permissions") + .description("List available permissions for a resource app") + .option("-i, --app-id ", "Resource app ID") + .option("-n, --display-name ", "Resource app display name") + .option("-f, --filter ", "Filter by permission name glob") + .action(async (options) => { + const output = await runListResourcePermissionsCommand(options); + renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns); + }); + + + // Parse arguments + await skAzTools.parseAsync(); } main().catch((err: unknown) => { const error = err as Error; console.error(`Error: ${error.message}`); - console.error(usage()); + //console.error(usage()); process.exit(1); }); diff --git a/src/cli/commands.ts b/src/cli/commands.ts deleted file mode 100644 index a41e9b0..0000000 --- a/src/cli/commands.ts +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT - -import { runGetTokenCommand } from "./commands/get-token.ts"; -import { runListAppGrantsCommand } from "./commands/list-app-grants.ts"; -import { runListAppPermissionsCommand } from "./commands/list-app-permissions.ts"; -import { runListAppsCommand } from "./commands/list-apps.ts"; -import { runListResourcePermissionsCommand } from "./commands/list-resource-permissions.ts"; -import { runLoginCommand } from "./commands/login.ts"; -import { runLogoutCommand } from "./commands/logout.ts"; -import { runRestCommand } from "./commands/rest.ts"; - -import type { CommandValues } from "./commands/types.ts"; - -export async function runCommand(command: string, values: CommandValues): Promise { - switch (command) { - case "login": - return runLoginCommand(values); - case "logout": - return runLogoutCommand(values); - case "list-apps": - return runListAppsCommand(values); - case "list-app-permissions": - return runListAppPermissionsCommand(values); - case "list-app-grants": - return runListAppGrantsCommand(values); - case "list-resource-permissions": - return runListResourcePermissionsCommand(values); - case "get-token": - return runGetTokenCommand(values); - case "rest": - return runRestCommand(values); - default: - throw new Error(`Unknown command: ${command}`); - } -} diff --git a/src/cli/commands/get-token.ts b/src/cli/commands/get-token.ts index 682c9a7..72d48e7 100644 --- a/src/cli/commands/get-token.ts +++ b/src/cli/commands/get-token.ts @@ -4,7 +4,9 @@ import { getAccessToken } from "../../azure/index.ts"; import { getDevOpsApiToken } from "../../devops/index.ts"; import { loadAuthConfig } from "../../index.ts"; -import type { CommandValues } from "./types.ts"; +type GetTokenOptions = { + type?: string; +}; export function usageGetToken(): string { return `Usage: sk-az-tools get-token --type|-t [global options] @@ -14,9 +16,9 @@ Options: } export async function runGetTokenCommand( - values: CommandValues, + options: GetTokenOptions, ): Promise { - const tokenType = (values.type ?? "").toString().trim().toLowerCase(); + const tokenType = (options.type ?? "").toString().trim().toLowerCase(); if (!tokenType) { throw new Error( "--type is required for get-token (allowed: azurerm, devops)", @@ -49,5 +51,5 @@ export async function runGetTokenCommand( }; } - throw new Error(`Invalid --type '${values.type}'. Allowed: azurerm, devops`); + throw new Error(`Invalid --type '${options.type}'. Allowed: azurerm, devops`); } diff --git a/src/cli/commands/list-app-grants.ts b/src/cli/commands/list-app-grants.ts index e3ae404..7aec1f2 100644 --- a/src/cli/commands/list-app-grants.ts +++ b/src/cli/commands/list-app-grants.ts @@ -2,7 +2,10 @@ import { listAppGrants } from "../../graph/app.ts"; import { getGraphClient } from "../../graph/index.ts"; -import type { CommandValues } from "./types.ts"; + +type ListAppGrantsOptions = { + appId?: string; +}; export function usageListAppGrants(): string { return `Usage: sk-az-tools list-app-grants --app-id|-i [global options] @@ -11,11 +14,11 @@ Options: --app-id, -i Application (client) ID (required)`; } -export async function runListAppGrantsCommand(values: CommandValues): Promise { - if (!values["app-id"]) { +export async function runListAppGrantsCommand(options: ListAppGrantsOptions): Promise { + if (!options.appId) { throw new Error("--app-id is required for list-app-grants"); } const client = await getGraphClient(); - return listAppGrants(client, values["app-id"]); + return listAppGrants(client, options.appId); } diff --git a/src/cli/commands/list-app-permissions.ts b/src/cli/commands/list-app-permissions.ts index 1a513a8..80f20bd 100644 --- a/src/cli/commands/list-app-permissions.ts +++ b/src/cli/commands/list-app-permissions.ts @@ -4,7 +4,13 @@ import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app. import { filterByPermissionName } from "./shared.ts"; import { getGraphClient } from "../../graph/index.ts"; -import type { CommandValues } from "./types.ts"; + +type ListAppPermissionsOptions = { + appId?: string; + resolve?: boolean; + short?: boolean; + filter?: string; +}; function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); @@ -33,20 +39,20 @@ Options: --filter, -f Filter by permission name glob`; } -export async function runListAppPermissionsCommand(values: CommandValues): Promise { - if (!values["app-id"]) { +export async function runListAppPermissionsCommand(options: ListAppPermissionsOptions): Promise { + if (!options.appId) { throw new Error("--app-id is required for list-app-permissions"); } const client = await getGraphClient(); - let result: unknown = values.resolve || values.filter - ? await listAppPermissionsResolved(client, values["app-id"]) - : await listAppPermissions(client, values["app-id"]); - if (values.short) { + let result: unknown = options.resolve || options.filter + ? await listAppPermissionsResolved(client, options.appId) + : await listAppPermissions(client, options.appId); + if (options.short) { result = omitColumns(result, ["resourceAppId", "permissionId"]); } - if (values.filter) { - result = filterByPermissionName(result as Array>, values.filter); + if (options.filter) { + result = filterByPermissionName(result as Array>, options.filter); } return result; } diff --git a/src/cli/commands/list-apps.ts b/src/cli/commands/list-apps.ts index f55a90c..40e7e90 100644 --- a/src/cli/commands/list-apps.ts +++ b/src/cli/commands/list-apps.ts @@ -3,26 +3,23 @@ import { listApps } from "../../graph/app.ts"; import { filterByDisplayName } from "./shared.ts"; import { getGraphClient } from "../../graph/index.ts"; -import type { CommandValues } from "./types.ts"; -export function usageListApps(): string { - return `Usage: sk-az-tools list-apps [--display-name|-n ] [--app-id|-i ] [--filter|-f ] [global options] +type ListAppsOptions = { + displayName?: string; + appId?: string; + filter?: string; +}; -Options: - --display-name, -n Get app by name - --app-id, -i Get app by id - --filter, -f Filter by app display name glob`; -} - -export async function runListAppsCommand(values: CommandValues): Promise { +export async function runListAppsCommand(options: ListAppsOptions): Promise { const client = await getGraphClient(); - let result = await listApps(client, values["display-name"], values["app-id"]); - if (values["app-id"] && result.length > 1) { - throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`); + let result = await listApps(client, options.displayName, options.appId); + + if (options.appId && result.length > 1) { + throw new Error(`Expected a single app for --app-id ${options.appId}, but got ${result.length}`); } - if (values.filter) { - result = filterByDisplayName(result, values.filter); + if (options.filter) { + result = filterByDisplayName(result, options.filter); } return result; } diff --git a/src/cli/commands/list-resource-permissions.ts b/src/cli/commands/list-resource-permissions.ts index 251c31f..4a8b812 100644 --- a/src/cli/commands/list-resource-permissions.ts +++ b/src/cli/commands/list-resource-permissions.ts @@ -3,7 +3,12 @@ import { listResourcePermissions } from "../../graph/app.ts"; import { getGraphClient } from "../../graph/index.ts"; import { filterByPermissionName } from "./shared.ts"; -import type { CommandValues } from "./types.ts"; + +type ListResourcePermissionsOptions = { + appId?: string; + displayName?: string; + filter?: string; +}; export function usageListResourcePermissions(): string { return `Usage: sk-az-tools list-resource-permissions [--app-id|-i | --display-name|-n ] [--filter|-f ] [global options] @@ -14,22 +19,22 @@ Options: --filter, -f Filter by permission name glob`; } -export async function runListResourcePermissionsCommand(values: CommandValues): Promise { - if (!values["app-id"] && !values["display-name"]) { +export async function runListResourcePermissionsCommand(options: ListResourcePermissionsOptions): Promise { + if (!options.appId && !options.displayName) { throw new Error("--app-id or --display-name is required for list-resource-permissions"); } - if (values["app-id"] && values["display-name"]) { + if (options.appId && options.displayName) { throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both"); } const client = await getGraphClient(); let result = await listResourcePermissions( client, - values["app-id"], - values["display-name"], + options.appId, + options.displayName, ); - if (values.filter) { - result = filterByPermissionName(result, values.filter); + if (options.filter) { + result = filterByPermissionName(result, options.filter); } return result; } diff --git a/src/cli/commands/login.ts b/src/cli/commands/login.ts index b0a410a..999e6e7 100644 --- a/src/cli/commands/login.ts +++ b/src/cli/commands/login.ts @@ -3,7 +3,13 @@ import { login } from "../../azure/index.ts"; import { loadAuthConfig } from "../../index.ts"; -import type { CommandValues } from "./types.ts"; +type LoginOptions = { + resources?: string; + useDeviceCode?: boolean; + noBrowser?: boolean; + browser?: string; + browserProfile?: string; +}; export function usageLogin(): string { return `Usage: sk-az-tools login [--resources ] [--use-device-code] [--no-browser] [--browser ] [--browser-profile ] [global options] @@ -16,15 +22,15 @@ Options: --browser-profile Chromium profile name (e.g. Default, "Profile 1")`; } -export async function runLoginCommand(values: CommandValues): Promise { +export async function runLoginCommand(options: LoginOptions): Promise { const config = await loadAuthConfig("public-config"); return login( config.tenantId, config.clientId, - values.resources, - Boolean(values["use-device-code"]), - Boolean(values["no-browser"]), - values.browser, - values["browser-profile"], + options.resources, + Boolean(options.useDeviceCode), + Boolean(options.noBrowser), + options.browser, + options.browserProfile, ); } diff --git a/src/cli/commands/logout.ts b/src/cli/commands/logout.ts index 838f1ad..4c23e78 100644 --- a/src/cli/commands/logout.ts +++ b/src/cli/commands/logout.ts @@ -3,7 +3,9 @@ import { logout } from "../../azure/index.ts"; import { loadAuthConfig } from "../../index.ts"; -import type { CommandValues } from "./types.ts"; +type LogoutOptions = { + all?: boolean; +}; export function usageLogout(): string { return `Usage: sk-az-tools logout [--all] [global options] @@ -12,7 +14,7 @@ Options: --all Clear login state and remove all cached accounts`; } -export async function runLogoutCommand(values: CommandValues): Promise { +export async function runLogoutCommand(options: LogoutOptions): Promise { const config = await loadAuthConfig("public-config"); - return logout(config.tenantId, config.clientId, Boolean(values.all)); + return logout(config.tenantId, config.clientId, Boolean(options.all)); } diff --git a/src/cli/commands/rest.ts b/src/cli/commands/rest.ts index e5c53c1..8e488e4 100644 --- a/src/cli/commands/rest.ts +++ b/src/cli/commands/rest.ts @@ -4,21 +4,6 @@ import { getAccessToken } from "../../azure/index.ts"; import { getDevOpsApiToken } from "../../devops/index.ts"; import { loadAuthConfig } from "../../index.ts"; -import type { CommandValues } from "./types.ts"; - -export function usageRest(): string { - return `Usage: sk-az-tools rest [--method ] --url [--header ] [global options] - -Options: - --method HTTP method (default: GET; examples: GET, POST, PATCH, DELETE) - --url Full URL to call - --header Extra request header; example: "Content-Type: application/json" - -Authorization is added automatically for: - management.azure.com Uses azurerm token - dev.azure.com Uses devops token`; -} - function parseHeaderLine( header?: string, ): { name: string; value: string } | null { @@ -70,24 +55,30 @@ async function getAutoAuthorizationHeader(url: URL): Promise { return `Bearer ${accessToken}`; } -export async function runRestCommand(values: CommandValues): Promise { - const method = - (values.method ?? "GET").toString().trim().toUpperCase() || "GET"; - const urlValue = (values.url ?? "").toString().trim(); +type httpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + +type restOptions = { + method?: httpMethod; + header?: string; +}; + +export async function runRestCommand(url: string, options: restOptions): Promise { + const method = options.method || "GET"; + const urlValue = (url ?? "").toString().trim(); if (!urlValue) { - throw new Error("--url is required for rest"); + throw new Error("URL is required for rest"); } let targetUrl: URL; try { targetUrl = new URL(urlValue); } catch { - throw new Error(`Invalid --url '${urlValue}'`); + throw new Error(`Invalid URL '${urlValue}'`); } const headers = new Headers(); - const customHeader = parseHeaderLine(values.header); + const customHeader = parseHeaderLine(options.header); if (customHeader) { headers.set(customHeader.name, customHeader.value); } @@ -105,9 +96,9 @@ export async function runRestCommand(values: CommandValues): Promise { }); const contentType = response.headers.get("content-type") ?? ""; - let body: unknown; + let body: string; if (contentType.toLowerCase().includes("application/json")) { - body = await response.json(); + body = JSON.stringify(await response.json()); } else { body = await response.text(); }