From 047e342842cd870f4e7ae6e02aa399630787bd5f Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Tue, 10 Mar 2026 07:41:47 +0100 Subject: [PATCH] Finished migration to commander.js, updated table rendering engine to use code blocks for resource Guids. --- docs/Commands.md | 41 ++++++++---- package-lock.json | 18 ++++-- package.json | 3 +- scripts/bump-patch.mjs | 5 +- src/cli.ts | 115 ++++++++++----------------------- src/cli/commands.ts | 21 ------ src/cli/commands/table-info.ts | 8 --- src/cli/commands/table.ts | 9 --- src/markdown.ts | 8 +++ 9 files changed, 90 insertions(+), 138 deletions(-) delete mode 100644 src/cli/commands.ts diff --git a/docs/Commands.md b/docs/Commands.md index 76b476e..2b27e5b 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -2,26 +2,41 @@ The `sk-tools` package provides generic CLI utilities not tied to a specific cloud provider. +## Global Options + +These options apply to all commands unless stated otherwise: + +- `--query`, `-q` - Apply JMESPath filter before output rendering. +- `--output`, `-o` - Output format: `table|t|alignedtable|at|prettytable|pt|tsv`. +- `--columns`, `-C` - Column selection for table outputs: + - `col1` - Select column (case-insensitive match), keep raw header label. + - `col1:` - Select column (case-insensitive match), use auto-generated header label. + - `col1: Label 1` - Select column (case-insensitive match), use custom header label. + - Prefix token with `=` for exact column-name match: `=col1`, `=col1:`, `=col1:Label`. + - Tokens are comma-separated and rendered in the specified order. +- `--help`, `-h` - Show help. + ## Table -**Command name:** `table` +**Command name:** `table` (alias: `t`) **Usage:** `sk-tools table [--from|-F ] [--columns|-C ] [global options]` **Options:** - `--from`, `-F` - Input format read from stdin. Default: `json`. -- `--columns`, `-C` - Column definition. Possible values: - - `col1` - Select column (case-insensitive match), keep raw header label. - - `col1:` - Select column (case-insensitive match), use auto-generated header label. - - `col1: Label 1` - Select column (case-insensitive match), use custom header label. - - Prefix token with `=` for exact column-name match: `=col1`, `=col1:`, `=col1:Label`. - - Tokens are comma-separated and rendered in the specified order. - -**Global options:** - -- `--query`, `-q` - JMESPath filter applied before rendering. -- `--output`, `-o` - Output format: `table|t|alignedtable|at|prettytable|pt|tsv`. -- `--help`, `-h` - Show help. +- `--columns`, `-C` - Column definition (see global options). **Description:** The `table` command transforms stdin data into tabular output. It accepts JSON, CSV, or TSV input and renders either Markdown table variants or TSV output. + +## Table Info + +**Command name:** `table-info` (alias: `ti`) + +**Usage:** `sk-tools table-info [--from|-F ]` + +**Options:** + +- `--from`, `-F` - Input format read from stdin. Default: `json`. + +**Description:** The `table-info` command reads stdin data and prints basic diagnostics: number of rows, number of columns, and inferred type for each column. diff --git a/package-lock.json b/package-lock.json index 1e3a079..0eb4ece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { + "commander": "^14.0.3", "d3-dsv": "^3.0.1", "jmespath": "^0.16.0", "semver": "^7.7.4", @@ -52,12 +53,12 @@ } }, "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": { @@ -85,6 +86,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/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/package.json b/package.json index e38de7a..69cc7df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slawek/sk-tools", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "files": [ "dist", @@ -17,6 +17,7 @@ }, "description": "A set of generic NodeJS utilities shared by Slawek tools.", "dependencies": { + "commander": "^14.0.3", "d3-dsv": "^3.0.1", "jmespath": "^0.16.0", "semver": "^7.7.4", diff --git a/scripts/bump-patch.mjs b/scripts/bump-patch.mjs index 5cb96cb..acd5fcc 100755 --- a/scripts/bump-patch.mjs +++ b/scripts/bump-patch.mjs @@ -36,8 +36,9 @@ const program = new Command(); program .name('bump-patch') .description('Bump the version in package.json') - .addOption(new Option('-r, --release-type ', 'Release type (major, minor, patch)', 'patch') - .choices('release-type', ['major', 'minor', 'patch']) + .addOption(new Option('-r, --release-type ', 'Release type (major, minor, patch)') + .choices(['major', 'minor', 'patch']) + .default('patch') ) .parse(process.argv); diff --git a/src/cli.ts b/src/cli.ts index 04c3542..560879e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,98 +1,53 @@ #!/usr/bin/env node // SPDX-License-Identifier: MIT -import { parseArgs } from "node:util"; +import { Command, Option } from "commander"; -import { runCommand } from "./cli/commands.ts"; -import { usageTableInfo } from "./cli/commands/table-info.ts"; -import { usageTable } from "./cli/commands/table.ts"; +import { runTableInfoCommand } from "./cli/commands/table-info.ts"; +import { runTableCommand } from "./cli/commands/table.ts"; import { renderCliOutput } from "./cli/utils.ts"; -type CliValues = { - help?: boolean; - from?: string; - query?: string; - columns?: string; - output?: string; - [key: string]: string | boolean | undefined; -}; - -function usage(): string { - return `Usage: sk-tools [options] - -Commands: - table, t Render stdin data as Markdown table - table-info, ti Print row/column stats and inferred column types - -Global options (all commands): - --query, -q - --output, -o table|t|alignedtable|at|prettytable|pt|tsv - --help, -h - -Use: sk-tools --help -or: sk-tools --help`; -} - -function usageCommand(command: string): string { - switch (command) { - case "table": - case "t": - return usageTable(); - case "table-info": - case "ti": - return usageTableInfo(); - default: - return `Unknown command: ${command}\n\n${usage()}`; - } -} - async function main(): Promise { - const argv = process.argv.slice(2); - const command = argv[0]; + const skTools = new Command(); - if (!command) { - console.log(usage()); - process.exit(0); - } + skTools + .name("sk-tools") + .description("A set of generic NodeJS utilities shared by Slawek tools.") + .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"])); - if (command === "-h" || command === "--help") { - const helpCommand = argv[1]; - console.log(helpCommand ? usageCommand(helpCommand) : usage()); - process.exit(0); - } + skTools + .command("table") + .alias("t") + .description("Render stdin data as Markdown table") + .addOption(new Option("-F, --from ", "Input format on stdin") + .choices(["json", "csv", "tsv"]) + .default("json")) + .action(async (options, command) => { + const allOptions = command.optsWithGlobals(); + const output = await runTableCommand(options); + renderCliOutput(output, allOptions.output ?? "alignedtable", allOptions.query, allOptions.columns); + }); - const { values } = parseArgs({ - args: argv.slice(1), - options: { - help: { type: "boolean", short: "h" }, - from: { type: "string", short: "F" }, - query: { type: "string", short: "q" }, - columns: { type: "string", short: "C" }, - output: { type: "string", short: "o" }, - }, - strict: true, - allowPositionals: false, - }); + skTools + .command("table-info") + .alias("ti") + .description("Print row/column stats and inferred column types") + .addOption(new Option("-F, --from ", "Input format on stdin") + .choices(["json", "csv", "tsv"]) + .default("json")) + .action(async (options) => { + const output = await runTableInfoCommand(options); + console.log(output); + }); - const typedValues = values as CliValues; - - if (typedValues.help) { - console.log(usageCommand(command)); - process.exit(0); - } - - const output = await runCommand(command, typedValues); - if (typeof output === "string") { - console.log(output); - return; - } - - renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns); + await skTools.parseAsync(process.argv); } main().catch((err: unknown) => { const error = err as Error; console.error(`Error: ${error.message}`); - console.error(usage()); process.exit(1); }); diff --git a/src/cli/commands.ts b/src/cli/commands.ts deleted file mode 100644 index 28873c5..0000000 --- a/src/cli/commands.ts +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -import { runTableCommand } from "./commands/table.ts"; -import { runTableInfoCommand } from "./commands/table-info.ts"; - -type CommandValues = { - [key: string]: string | boolean | undefined; -}; - -export async function runCommand(command: string, values: CommandValues): Promise { - switch (command) { - case "table": - case "t": - return runTableCommand(values); - case "table-info": - case "ti": - return runTableInfoCommand(values); - default: - throw new Error(`Unknown command: ${command}`); - } -} diff --git a/src/cli/commands/table-info.ts b/src/cli/commands/table-info.ts index 4f87912..cc26263 100644 --- a/src/cli/commands/table-info.ts +++ b/src/cli/commands/table-info.ts @@ -7,7 +7,6 @@ import { type TableInfoCommandValues = { from?: string; - [key: string]: string | boolean | undefined; }; type RowObject = Record; @@ -102,13 +101,6 @@ function buildInfo(input: unknown): string { return lines.join("\n"); } -export function usageTableInfo(): string { - return `Usage: sk-tools table-info [--from|-F ] - -Options: - --from, -F Input format on stdin (default: json)`; -} - export async function runTableInfoCommand(values: TableInfoCommandValues): Promise { const from = (values.from ?? "json").toString().trim().toLowerCase(); diff --git a/src/cli/commands/table.ts b/src/cli/commands/table.ts index 7476023..807d59c 100644 --- a/src/cli/commands/table.ts +++ b/src/cli/commands/table.ts @@ -7,17 +7,8 @@ import { type TableCommandValues = { from?: string; - [key: string]: string | boolean | undefined; }; -export function usageTable(): string { - return `Usage: sk-tools table [--from|-F ] [--columns|-C ] [global options] - -Options: - --from, -F Input format on stdin (default: json) - --columns, -C Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`; -} - export async function runTableCommand(values: TableCommandValues): Promise { const from = (values.from ?? "json").toString().trim().toLowerCase(); diff --git a/src/markdown.ts b/src/markdown.ts index 416ee0e..0336831 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -27,6 +27,14 @@ function formatCell(value: unknown): string { const inlineCodePredicates = [ (value: Scalar): boolean => typeof value === "string" && validateUuid(value), + (value: Scalar): boolean => { + if (typeof value !== "string" || !value.toLowerCase().startsWith("/subscriptions/")) { + return false; + } + const rest = value.slice("/subscriptions/".length); + const subscriptionId = rest.split("/", 1)[0]; + return validateUuid(subscriptionId); + }, (value: Scalar): boolean => typeof value === "string" && /^(?:\d{1,3}\.){3}\d{1,3}(?:\/(?:\d{1,2}|(?:\d{1,3}\.){3}\d{1,3}))?$/.test(value),