Refactored CLI to support multiple commands as sk-az-tools do. Added table-info command.
All checks were successful
build / build (push) Successful in 10s
All checks were successful
build / build (push) Successful in 10s
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-tools",
|
"name": "@slawek/sk-tools",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@slawek/sk-tools",
|
"name": "@slawek/sk-tools",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-dsv": "^3.0.1",
|
"d3-dsv": "^3.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-tools",
|
"name": "@slawek/sk-tools",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
47
src/cli.ts
47
src/cli.ts
@@ -3,11 +3,10 @@
|
|||||||
|
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
|
|
||||||
import {
|
import { runCommand } from "./cli/commands.ts";
|
||||||
renderCliOutput,
|
import { usageTableInfo } from "./cli/commands/table-info.ts";
|
||||||
readCsvFromStdin,
|
import { usageTable } from "./cli/commands/table.ts";
|
||||||
readJsonFromStdin,
|
import { renderCliOutput } from "./cli/utils.ts";
|
||||||
} from "./cli/utils.ts";
|
|
||||||
|
|
||||||
type CliValues = {
|
type CliValues = {
|
||||||
help?: boolean;
|
help?: boolean;
|
||||||
@@ -22,7 +21,8 @@ function usage(): string {
|
|||||||
return `Usage: sk-tools <command> [options]
|
return `Usage: sk-tools <command> [options]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
table Render stdin data as Markdown table
|
table, t Render stdin data as Markdown table
|
||||||
|
table-info, ti Print row/column stats and inferred column types
|
||||||
|
|
||||||
Global options (all commands):
|
Global options (all commands):
|
||||||
--query, -q <jmespath>
|
--query, -q <jmespath>
|
||||||
@@ -33,39 +33,19 @@ Use: sk-tools --help <command>
|
|||||||
or: sk-tools <command> --help`;
|
or: sk-tools <command> --help`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageTable(): string {
|
|
||||||
return `Usage: sk-tools table [--from|-F <json|csv|tsv>] [--columns|-C <columns>] [global options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--from, -F <json|csv|tsv> Input format on stdin (default: json)
|
|
||||||
--columns, -C <value> Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function usageCommand(command: string): string {
|
function usageCommand(command: string): string {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "table":
|
case "table":
|
||||||
|
case "t":
|
||||||
return usageTable();
|
return usageTable();
|
||||||
|
case "table-info":
|
||||||
|
case "ti":
|
||||||
|
return usageTableInfo();
|
||||||
default:
|
default:
|
||||||
return `Unknown command: ${command}\n\n${usage()}`;
|
return `Unknown command: ${command}\n\n${usage()}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTableCommand(values: CliValues): Promise<unknown> {
|
|
||||||
const from = (values.from ?? "json").toString().trim().toLowerCase();
|
|
||||||
|
|
||||||
if (from === "json") {
|
|
||||||
return readJsonFromStdin();
|
|
||||||
}
|
|
||||||
if (from === "csv") {
|
|
||||||
return readCsvFromStdin(",");
|
|
||||||
}
|
|
||||||
if (from === "tsv") {
|
|
||||||
return readCsvFromStdin("\t");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
const command = argv[0];
|
const command = argv[0];
|
||||||
@@ -101,11 +81,12 @@ async function main(): Promise<void> {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command !== "table") {
|
const output = await runCommand(command, typedValues);
|
||||||
throw new Error(`Unknown command: ${command}`);
|
if (typeof output === "string") {
|
||||||
|
console.log(output);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = await runTableCommand(typedValues);
|
|
||||||
renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns);
|
renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/cli/commands.ts
Normal file
21
src/cli/commands.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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<unknown> {
|
||||||
|
switch (command) {
|
||||||
|
case "table":
|
||||||
|
case "t":
|
||||||
|
return runTableCommand(values);
|
||||||
|
case "table-info":
|
||||||
|
case "ti":
|
||||||
|
return runTableInfoCommand(values);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown command: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/cli/commands/table-info.ts
Normal file
126
src/cli/commands/table-info.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import {
|
||||||
|
readCsvFromStdin,
|
||||||
|
readJsonFromStdin,
|
||||||
|
} from "../utils.ts";
|
||||||
|
|
||||||
|
type TableInfoCommandValues = {
|
||||||
|
from?: string;
|
||||||
|
[key: string]: string | boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
type ValueType = "string" | "number" | "boolean" | "object" | "array" | "null" | "unknown";
|
||||||
|
|
||||||
|
function getType(value: unknown): ValueType {
|
||||||
|
if (value === null) return "null";
|
||||||
|
if (Array.isArray(value)) return "array";
|
||||||
|
if (value === undefined) return "unknown";
|
||||||
|
if (typeof value === "string") return "string";
|
||||||
|
if (typeof value === "number") return "number";
|
||||||
|
if (typeof value === "boolean") return "boolean";
|
||||||
|
if (typeof value === "object") return "object";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRows(input: unknown): { rowCount: number; rows: RowObject[] } {
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return {
|
||||||
|
rowCount: input.length,
|
||||||
|
rows: input.map((item) =>
|
||||||
|
item && typeof item === "object" && !Array.isArray(item)
|
||||||
|
? item as RowObject
|
||||||
|
: { value: item }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input && typeof input === "object") {
|
||||||
|
return {
|
||||||
|
rowCount: 1,
|
||||||
|
rows: [input as RowObject],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowCount: 1,
|
||||||
|
rows: [{ value: input }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferColumnType(rows: RowObject[], column: string): string {
|
||||||
|
const kinds = new Set<ValueType>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!(column in row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = getType(row[column]);
|
||||||
|
if (kind !== "unknown") {
|
||||||
|
kinds.add(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kinds.size === 0) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kinds.size === 1) {
|
||||||
|
return [...kinds][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kinds.size === 2 && kinds.has("null")) {
|
||||||
|
return [...kinds].find((kind) => kind !== "null") ?? "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `mixed(${[...kinds].join("|")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInfo(input: unknown): string {
|
||||||
|
const { rowCount, rows } = normalizeRows(input);
|
||||||
|
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`Number of columns: ${columns.length}`,
|
||||||
|
`Number of rows: ${rowCount}`,
|
||||||
|
"",
|
||||||
|
"Columns:",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (columns.length === 0) {
|
||||||
|
lines.push("- (none)");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const column of columns) {
|
||||||
|
lines.push(`- ${column}: ${inferColumnType(rows, column)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usageTableInfo(): string {
|
||||||
|
return `Usage: sk-tools table-info [--from|-F <json|csv|tsv>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--from, -F <json|csv|tsv> Input format on stdin (default: json)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runTableInfoCommand(values: TableInfoCommandValues): Promise<string> {
|
||||||
|
const from = (values.from ?? "json").toString().trim().toLowerCase();
|
||||||
|
|
||||||
|
if (from === "json") {
|
||||||
|
return buildInfo(await readJsonFromStdin());
|
||||||
|
}
|
||||||
|
if (from === "csv") {
|
||||||
|
return buildInfo(await readCsvFromStdin(","));
|
||||||
|
}
|
||||||
|
if (from === "tsv") {
|
||||||
|
return buildInfo(await readCsvFromStdin("\t"));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`);
|
||||||
|
}
|
||||||
35
src/cli/commands/table.ts
Normal file
35
src/cli/commands/table.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import {
|
||||||
|
readCsvFromStdin,
|
||||||
|
readJsonFromStdin,
|
||||||
|
} from "../utils.ts";
|
||||||
|
|
||||||
|
type TableCommandValues = {
|
||||||
|
from?: string;
|
||||||
|
[key: string]: string | boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function usageTable(): string {
|
||||||
|
return `Usage: sk-tools table [--from|-F <json|csv|tsv>] [--columns|-C <columns>] [global options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--from, -F <json|csv|tsv> Input format on stdin (default: json)
|
||||||
|
--columns, -C <value> Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runTableCommand(values: TableCommandValues): Promise<unknown> {
|
||||||
|
const from = (values.from ?? "json").toString().trim().toLowerCase();
|
||||||
|
|
||||||
|
if (from === "json") {
|
||||||
|
return readJsonFromStdin();
|
||||||
|
}
|
||||||
|
if (from === "csv") {
|
||||||
|
return readCsvFromStdin(",");
|
||||||
|
}
|
||||||
|
if (from === "tsv") {
|
||||||
|
return readCsvFromStdin("\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`);
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ function isScalar(value: unknown): value is Scalar {
|
|||||||
return value == null || typeof value !== "object";
|
return value == null || typeof value !== "object";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } {
|
export function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } {
|
||||||
let rows: Array<Record<string, unknown>>;
|
let rows: Array<Record<string, unknown>>;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
rows = value.map((item) =>
|
rows = value.map((item) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user