@bunizao/cli-kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/dist/output.d.ts CHANGED
@@ -10,6 +10,10 @@ export declare function render(value: unknown, options: {
10
10
  format: OutputFormat;
11
11
  fields?: readonly string[];
12
12
  columns?: readonly [string, string][];
13
+ /** Terminal width the table has to fit into. Omitted means unlimited. */
14
+ width?: number;
15
+ /** Indent JSON. Readable on a terminal, wasted bytes in a pipe. */
16
+ pretty?: boolean;
13
17
  }): string;
14
18
  export declare function writeOutput(text: string, options: {
15
19
  output?: string;
package/dist/output.js CHANGED
@@ -10,11 +10,12 @@ export function resolveFormat(options, isTty) {
10
10
  }
11
11
  export function render(value, options) {
12
12
  const filtered = options.fields?.length ? selectFields(value, options.fields) : value;
13
- if (options.format === "json")
14
- return `${JSON.stringify(filtered, null, 2)}\n`;
13
+ if (options.format === "json") {
14
+ return `${options.pretty === false ? JSON.stringify(filtered) : JSON.stringify(filtered, null, 2)}\n`;
15
+ }
15
16
  if (options.format === "yaml")
16
17
  return stringifyYaml(filtered);
17
- return renderTable(filtered, options.columns);
18
+ return renderTable(filtered, options.columns, options.width);
18
19
  }
19
20
  export async function writeOutput(text, options) {
20
21
  if (options.output) {
@@ -41,22 +42,81 @@ function selectFields(value, fields) {
41
42
  return value;
42
43
  return Object.fromEntries(fields.filter((field) => field in value).map((field) => [field, value[field]]));
43
44
  }
44
- function renderTable(value, columns) {
45
+ /** Narrower than this and a column carries no information, so overflow is the lesser evil. */
46
+ const MIN_COLUMN = 8;
47
+ const GAP = " ";
48
+ function renderTable(value, columns, width) {
45
49
  const rows = Array.isArray(value) ? value : [value];
46
50
  if (rows.length === 0)
47
51
  return "";
48
52
  if (!rows.every(isRecord))
49
53
  return `${rows.map(String).join("\n")}\n`;
50
- const selected = columns ?? Object.keys(rows[0] ?? {}).map((key) => [key, key]);
51
- const values = rows.map((row) => selected.map(([key]) => formatCell(row[key])));
52
- const widths = selected.map(([, label], index) => Math.max(label.length, ...values.map((row) => row[index]?.length ?? 0)));
53
- const line = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
54
+ const selected = columns ?? columnsOf(rows);
55
+ const values = rows.map((row) => selected.map(([key]) => formatCell(valueAt(row, key))));
56
+ const natural = selected.map(([, label], index) => Math.max(label.length, ...values.map((row) => row[index]?.length ?? 0)));
57
+ const widths = fitWidths(natural, width);
58
+ const line = (cells) => cells.map((cell, index) => truncate(cell, widths[index] ?? 0).padEnd(widths[index] ?? 0)).join(GAP).trimEnd();
54
59
  return `${line(selected.map(([, label]) => label))}\n${values.map(line).join("\n")}\n`;
55
60
  }
61
+ /** A column key may reach into a nested object, as "unit.code". */
62
+ function valueAt(row, key) {
63
+ if (key in row)
64
+ return row[key];
65
+ let current = row;
66
+ for (const step of key.split(".")) {
67
+ if (!isRecord(current))
68
+ return undefined;
69
+ current = current[step];
70
+ }
71
+ return current;
72
+ }
73
+ /**
74
+ * Every key any row has, in the order the rows introduce them. Taking the keys of
75
+ * the first row alone drops whatever that row happened to leave out, which is a
76
+ * silent hole in the table for anyone who omits empty fields.
77
+ */
78
+ function columnsOf(rows) {
79
+ const keys = new Set();
80
+ for (const row of rows)
81
+ for (const key of Object.keys(row))
82
+ keys.add(key);
83
+ return [...keys].map((key) => [key, key]);
84
+ }
85
+ /**
86
+ * Shave the widest column a character at a time until the table fits. Truncation is
87
+ * visible; dropping a column is not, so columns are kept even when the budget runs out.
88
+ */
89
+ function fitWidths(natural, width) {
90
+ const widths = [...natural];
91
+ if (!width || width <= 0)
92
+ return widths;
93
+ const budget = width - GAP.length * (widths.length - 1);
94
+ let total = widths.reduce((sum, value) => sum + value, 0);
95
+ while (total > budget) {
96
+ const widest = widths.reduce((best, value, index) => (value > (widths[best] ?? 0) ? index : best), 0);
97
+ if ((widths[widest] ?? 0) <= MIN_COLUMN)
98
+ break;
99
+ widths[widest] = (widths[widest] ?? 0) - 1;
100
+ total -= 1;
101
+ }
102
+ return widths;
103
+ }
104
+ function truncate(cell, width) {
105
+ return width > 0 && cell.length > width ? `${cell.slice(0, width - 1)}…` : cell;
106
+ }
107
+ /** A cell is one line of a table, so nested values are flattened rather than dumped as JSON. */
56
108
  function formatCell(value) {
57
109
  if (value === null || value === undefined)
58
110
  return "";
59
- return typeof value === "object" ? JSON.stringify(value) : String(value);
111
+ if (Array.isArray(value))
112
+ return value.map(formatCell).filter(Boolean).join(", ");
113
+ if (isRecord(value)) {
114
+ return Object.entries(value)
115
+ .filter(([, item]) => item !== null && item !== undefined && item !== "")
116
+ .map(([key, item]) => `${key}=${formatCell(item)}`)
117
+ .join(" ");
118
+ }
119
+ return String(value);
60
120
  }
61
121
  function isRecord(value) {
62
122
  return typeof value === "object" && value !== null && !Array.isArray(value);
package/dist/program.js CHANGED
@@ -22,7 +22,6 @@ export function createProgram(meta) {
22
22
  .option("--table", "Emit a human-readable table")
23
23
  .option("--fields <fields>", "Select top-level fields")
24
24
  .option("-o, --output <file>", "Write output to a file")
25
- .option("-q, --quiet", "Suppress non-essential diagnostics")
26
25
  .option("--verbose", "Enable debug logging")
27
26
  .option("--no-color", "Disable color output")
28
27
  .option("-y, --yes", "Confirm mutations non-interactively")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunizao/cli-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared command-line contract for Bunizao CLI tools",
5
5
  "type": "module",
6
6
  "exports": {