@lewishowles/cli-style 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/adapters/bash/cli-style.sh +50 -0
  4. package/adapters/python/cli_style.py +104 -0
  5. package/bin/cli-style.js +82 -0
  6. package/examples/diagnostic.mjs +30 -0
  7. package/examples/diagnostic.py +38 -0
  8. package/examples/diagnostic.sh +29 -0
  9. package/examples/gallery.mjs +18 -0
  10. package/package.json +42 -0
  11. package/src/capability/resolve-profile.js +69 -0
  12. package/src/capability/terminal.js +105 -0
  13. package/src/cli/gallery-command.js +238 -0
  14. package/src/cli/help.js +35 -0
  15. package/src/cli/render-command.js +283 -0
  16. package/src/create-cli-style.js +53 -0
  17. package/src/formatters/ansi.js +154 -0
  18. package/src/gallery/render-gallery.js +758 -0
  19. package/src/index.js +52 -0
  20. package/src/patterns/agent-transcript.js +180 -0
  21. package/src/patterns/audit-finding.js +107 -0
  22. package/src/patterns/command-result.js +91 -0
  23. package/src/patterns/compact-data-table.js +67 -0
  24. package/src/patterns/confirmation-result.js +88 -0
  25. package/src/patterns/diagnostic-report.js +144 -0
  26. package/src/patterns/helpers.js +106 -0
  27. package/src/patterns/next-step-block.js +118 -0
  28. package/src/patterns/task-summary.js +59 -0
  29. package/src/primitives/bar-chart.js +131 -0
  30. package/src/primitives/chip.js +33 -0
  31. package/src/primitives/divider.js +64 -0
  32. package/src/primitives/feedback.js +83 -0
  33. package/src/primitives/panel.js +100 -0
  34. package/src/primitives/progress-bar.js +121 -0
  35. package/src/primitives/row.js +80 -0
  36. package/src/primitives/status.js +39 -0
  37. package/src/primitives/step-progress.js +184 -0
  38. package/src/primitives/table.js +239 -0
  39. package/src/profiles/profiles.js +24 -0
  40. package/src/theme/colours.js +141 -0
  41. package/src/theme/results.js +98 -0
@@ -0,0 +1,239 @@
1
+ import { foreground } from "../formatters/ansi.js";
2
+ import { tableColours } from "../theme/colours.js";
3
+
4
+ // Tables assume a standard terminal width when no capability is provided.
5
+ const defaultWidth = 80;
6
+
7
+ // Columns use a compact but readable gap.
8
+ const columnSeparator = " ";
9
+
10
+ /**
11
+ * Render tabular data with a narrow-width key/value fallback.
12
+ *
13
+ * @param {object} options
14
+ * Rendering options.
15
+ * @param {boolean} options.colour
16
+ * Whether ANSI colour should be applied.
17
+ * @param {object[]} options.columns
18
+ * Column keys and labels.
19
+ * @param {object[]} options.rows
20
+ * Table records.
21
+ * @param {boolean} options.unicode
22
+ * Whether Unicode rule characters should be used.
23
+ * @param {number} options.width
24
+ * Available output width.
25
+ * @returns {string}
26
+ * Rendered table.
27
+ */
28
+ export function table(options = {}) {
29
+ const columns = normaliseColumns(options.columns);
30
+ const rows = normaliseRows(options.rows);
31
+
32
+ if (columns.length === 0 || rows.length === 0) {
33
+ return "";
34
+ }
35
+
36
+ const widths = columnWidths(columns, rows);
37
+ const naturalWidth = widths.reduce((total, width) => total + width, 0)
38
+ + (columnSeparator.length * (columns.length - 1));
39
+ const availableWidth = Number.isFinite(options.width) ? options.width : defaultWidth;
40
+
41
+ if (naturalWidth > availableWidth) {
42
+ return renderBlocks(columns, rows, options);
43
+ }
44
+
45
+ return renderTable(columns, rows, widths, options);
46
+ }
47
+
48
+ /**
49
+ * Return columns with usable keys and labels.
50
+ *
51
+ * @param {object[]} columns
52
+ * Requested columns.
53
+ * @returns {object[]}
54
+ * Valid columns.
55
+ */
56
+ function normaliseColumns(columns) {
57
+ if (!Array.isArray(columns)) {
58
+ return [];
59
+ }
60
+
61
+ return columns.filter((column) => (
62
+ column !== null
63
+ && typeof column === "object"
64
+ && typeof column.key === "string"
65
+ && column.key !== ""
66
+ && typeof column.label === "string"
67
+ && column.label !== ""
68
+ ));
69
+ }
70
+
71
+ /**
72
+ * Return object records suitable for rendering.
73
+ *
74
+ * @param {object[]} rows
75
+ * Requested rows.
76
+ * @returns {object[]}
77
+ * Valid rows.
78
+ */
79
+ function normaliseRows(rows) {
80
+ if (!Array.isArray(rows)) {
81
+ return [];
82
+ }
83
+
84
+ return rows.filter((row) => row !== null && typeof row === "object");
85
+ }
86
+
87
+ /**
88
+ * Calculate the display width of each column.
89
+ *
90
+ * @param {object[]} columns
91
+ * Table columns.
92
+ * @param {object[]} rows
93
+ * Table records.
94
+ * @returns {number[]}
95
+ * Column widths.
96
+ */
97
+ function columnWidths(columns, rows) {
98
+ return columns.map((column) => Math.max(
99
+ column.label.length,
100
+ ...rows.map((row) => cellValue(row[column.key]).length),
101
+ ));
102
+ }
103
+
104
+ /**
105
+ * Render a wide aligned table.
106
+ *
107
+ * @param {object[]} columns
108
+ * Table columns.
109
+ * @param {object[]} rows
110
+ * Table records.
111
+ * @param {number[]} widths
112
+ * Column widths.
113
+ * @param {object} options
114
+ * Rendering options.
115
+ * @returns {string}
116
+ * Rendered table.
117
+ */
118
+ function renderTable(columns, rows, widths, options) {
119
+ const header = columns
120
+ .map((column, index) => renderHeader(
121
+ padColumn(column.label, widths[index], index, columns.length),
122
+ options,
123
+ ))
124
+ .join(columnSeparator);
125
+ const ruleCharacter = options.unicode === false ? "-" : "─";
126
+ const rule = widths
127
+ .map((width) => ruleCharacter.repeat(width))
128
+ .join(columnSeparator);
129
+ const renderedRule = options.colour === true
130
+ ? foreground(rule, tableColours.border, options)
131
+ : rule;
132
+ const body = rows.map((row) => columns
133
+ .map((column, index) => renderCell(
134
+ padColumn(cellValue(row[column.key]), widths[index], index, columns.length),
135
+ index,
136
+ options,
137
+ ))
138
+ .join(columnSeparator));
139
+
140
+ return [header, renderedRule, ...body].join("\n");
141
+ }
142
+
143
+ /**
144
+ * Pad columns except the final visible value.
145
+ *
146
+ * @param {string} value
147
+ * Column text.
148
+ * @param {number} width
149
+ * Calculated column width.
150
+ * @param {number} index
151
+ * Zero-based column position.
152
+ * @param {number} columnCount
153
+ * Total visible columns.
154
+ * @returns {string}
155
+ * Padded column text.
156
+ */
157
+ function padColumn(value, width, index, columnCount) {
158
+ return index === columnCount - 1 ? value : value.padEnd(width, " ");
159
+ }
160
+
161
+ /**
162
+ * Render records as separated key/value blocks.
163
+ *
164
+ * @param {object[]} columns
165
+ * Table columns.
166
+ * @param {object[]} rows
167
+ * Table records.
168
+ * @param {object} options
169
+ * Rendering options.
170
+ * @returns {string}
171
+ * Rendered key/value blocks.
172
+ */
173
+ function renderBlocks(columns, rows, options) {
174
+ const labelWidth = Math.max(...columns.map((column) => column.label.length));
175
+
176
+ return rows.map((row) => columns
177
+ .map((column) => {
178
+ const label = renderHeader(column.label.padEnd(labelWidth, " "), options);
179
+ const value = renderCell(cellValue(row[column.key]), 0, options);
180
+
181
+ return `${label}${columnSeparator}${value}`;
182
+ })
183
+ .join("\n"))
184
+ .join("\n\n");
185
+ }
186
+
187
+ /**
188
+ * Convert a cell value to display text.
189
+ *
190
+ * @param {*} value
191
+ * Cell value.
192
+ * @returns {string}
193
+ * Display text.
194
+ */
195
+ function cellValue(value) {
196
+ return value === null || value === undefined ? "" : String(value);
197
+ }
198
+
199
+ /**
200
+ * Render table header text.
201
+ *
202
+ * @param {string} value
203
+ * Header text.
204
+ * @param {object} options
205
+ * Rendering options.
206
+ * @returns {string}
207
+ * Rendered header.
208
+ */
209
+ function renderHeader(value, options) {
210
+ if (options.colour !== true) {
211
+ return value;
212
+ }
213
+
214
+ return foreground(value, tableColours.header, options);
215
+ }
216
+
217
+ /**
218
+ * Render table cell text.
219
+ *
220
+ * @param {string} value
221
+ * Cell text.
222
+ * @param {number} columnIndex
223
+ * Zero-based column position.
224
+ * @param {object} options
225
+ * Rendering options.
226
+ * @returns {string}
227
+ * Rendered cell.
228
+ */
229
+ function renderCell(value, columnIndex, options) {
230
+ if (options.colour !== true) {
231
+ return value;
232
+ }
233
+
234
+ const colour = columnIndex === 0
235
+ ? tableColours.primaryText
236
+ : tableColours.secondaryText;
237
+
238
+ return foreground(value, colour, options);
239
+ }
@@ -0,0 +1,24 @@
1
+ // Supported output profiles for terminal rendering.
2
+ export const profiles = {
3
+ AGENT: "agent",
4
+ CI: "ci",
5
+ DIAGNOSTIC: "diagnostic",
6
+ HUMAN: "human",
7
+ JSON: "json",
8
+ PLAIN: "plain",
9
+ };
10
+
11
+ // Profile names accepted by public APIs and CLI flags.
12
+ const profileValues = new Set(Object.values(profiles));
13
+
14
+ /**
15
+ * Check whether a profile name is supported.
16
+ *
17
+ * @param {string} profile
18
+ * Profile name to check.
19
+ * @returns {boolean}
20
+ * Whether the profile is supported.
21
+ */
22
+ export function isProfile(profile) {
23
+ return profileValues.has(profile);
24
+ }
@@ -0,0 +1,141 @@
1
+ // Semantic colour tokens for terminal output and generated previews.
2
+ export const colourTokens = {
3
+ accent: "#c58cff",
4
+ background: "#070b0d",
5
+ border: "#2a3a42",
6
+ danger: "#ff7272",
7
+ info: "#8bbdff",
8
+ muted: "#94a3aa",
9
+ success: "#8fdf72",
10
+ surface: "#0f181b",
11
+ surfaceRaised: "#132024",
12
+ text: "#e8eef0",
13
+ warning: "#f4bd5f",
14
+ };
15
+
16
+ // Terminal frame colours for gallery-style previews.
17
+ export const terminalColours = {
18
+ background: "background",
19
+ border: "border",
20
+ title: "muted",
21
+ trafficLights: {
22
+ close: "#ff7272",
23
+ minimise: "#f4bd5f",
24
+ zoom: "#8fdf72",
25
+ },
26
+ };
27
+
28
+ // Chip and pill colour pairs for speaker labels and compact states.
29
+ export const chipColours = {
30
+ agent: {
31
+ background: "#3a2815",
32
+ foreground: "warning",
33
+ },
34
+ danger: {
35
+ background: "#3c1d22",
36
+ foreground: "danger",
37
+ },
38
+ info: {
39
+ background: "#14273b",
40
+ foreground: "info",
41
+ },
42
+ neutral: {
43
+ background: "#172126",
44
+ foreground: "muted",
45
+ },
46
+ success: {
47
+ background: "#18351f",
48
+ foreground: "success",
49
+ },
50
+ user: {
51
+ background: "#1f3b1f",
52
+ foreground: "success",
53
+ },
54
+ warning: {
55
+ background: "#3a2815",
56
+ foreground: "warning",
57
+ },
58
+ };
59
+
60
+ // Panel colours for grouped reports and agent responses.
61
+ export const panelColours = {
62
+ background: "surface",
63
+ border: "border",
64
+ danger: "danger",
65
+ info: "info",
66
+ success: "success",
67
+ warning: "warning",
68
+ };
69
+
70
+ // Table colours for dense operational data.
71
+ export const tableColours = {
72
+ border: "border",
73
+ header: "muted",
74
+ primaryText: "text",
75
+ secondaryText: "muted",
76
+ };
77
+
78
+ // Chart colours for simple bars and positive/negative trend segments.
79
+ export const chartColours = {
80
+ barNegative: "#c48745",
81
+ barPositive: "success",
82
+ barWarning: "warning",
83
+ label: "muted",
84
+ value: "text",
85
+ };
86
+
87
+ // Prompt colours for lightweight interactive flows.
88
+ export const promptColours = {
89
+ active: "success",
90
+ inactive: "text",
91
+ marker: "success",
92
+ };
93
+
94
+ // Result tones map semantic outcomes onto colour token names.
95
+ export const toneColours = {
96
+ danger: "danger",
97
+ info: "info",
98
+ muted: "muted",
99
+ success: "success",
100
+ warning: "warning",
101
+ };
102
+
103
+ /**
104
+ * Resolve a colour token by name.
105
+ *
106
+ * @param {string} colourName
107
+ * Colour token name.
108
+ * @returns {string|null}
109
+ * Hex colour value when supported.
110
+ */
111
+ export function getColourToken(colourName) {
112
+ return colourTokens[colourName] ?? null;
113
+ }
114
+
115
+ /**
116
+ * Resolve a tone to a colour token value.
117
+ *
118
+ * @param {string} tone
119
+ * Tone name to resolve.
120
+ * @returns {string|null}
121
+ * Hex colour value when supported.
122
+ */
123
+ export function getToneColour(tone) {
124
+ return getColourToken(toneColours[tone]);
125
+ }
126
+
127
+ /**
128
+ * Resolve a direct hex colour or colour token reference.
129
+ *
130
+ * @param {string} colour
131
+ * Hex colour value or colour token name.
132
+ * @returns {string|null}
133
+ * Hex colour value when supported.
134
+ */
135
+ export function resolveColourValue(colour) {
136
+ if (colour?.startsWith("#")) {
137
+ return colour;
138
+ }
139
+
140
+ return getColourToken(colour);
141
+ }
@@ -0,0 +1,98 @@
1
+ // Result states shared by primitive and pattern renderers.
2
+ export const resultTypes = {
3
+ FAILED: "failed",
4
+ PARTIAL: "partial",
5
+ SKIPPED: "skipped",
6
+ SUCCESS: "success",
7
+ UNKNOWN: "unknown",
8
+ WARNING: "warning",
9
+ };
10
+
11
+ // Display tokens for result states before colour rendering exists.
12
+ export const resultTokens = {
13
+ [resultTypes.FAILED]: {
14
+ exitCode: 1,
15
+ label: "Failed",
16
+ symbols: {
17
+ ascii: "x",
18
+ unicode: "×",
19
+ },
20
+ tone: "danger",
21
+ },
22
+ [resultTypes.PARTIAL]: {
23
+ exitCode: null,
24
+ label: "Partial",
25
+ symbols: {
26
+ ascii: "!",
27
+ unicode: "◐",
28
+ },
29
+ tone: "warning",
30
+ },
31
+ [resultTypes.SKIPPED]: {
32
+ exitCode: null,
33
+ label: "Skipped",
34
+ symbols: {
35
+ ascii: "-",
36
+ unicode: "–",
37
+ },
38
+ tone: "muted",
39
+ },
40
+ [resultTypes.SUCCESS]: {
41
+ exitCode: 0,
42
+ label: "Success",
43
+ symbols: {
44
+ ascii: "OK",
45
+ unicode: "✓",
46
+ },
47
+ tone: "success",
48
+ },
49
+ [resultTypes.UNKNOWN]: {
50
+ exitCode: null,
51
+ label: "Unknown",
52
+ symbols: {
53
+ ascii: "?",
54
+ unicode: "?",
55
+ },
56
+ tone: "muted",
57
+ },
58
+ [resultTypes.WARNING]: {
59
+ exitCode: null,
60
+ label: "Warning",
61
+ symbols: {
62
+ ascii: "!",
63
+ unicode: "⚠",
64
+ },
65
+ tone: "warning",
66
+ },
67
+ };
68
+
69
+ /**
70
+ * Resolve the token for a result type.
71
+ *
72
+ * @param {string} resultType
73
+ * Result type to resolve.
74
+ * @returns {object}
75
+ * Matching result token, or the unknown token for unsupported values.
76
+ */
77
+ export function getResultToken(resultType) {
78
+ return resultTokens[resultType] ?? resultTokens[resultTypes.UNKNOWN];
79
+ }
80
+
81
+ /**
82
+ * Resolve the display symbol for a result type.
83
+ *
84
+ * @param {string} resultType
85
+ * Result type to resolve.
86
+ * @param {object} options
87
+ * Symbol rendering options.
88
+ * @param {boolean} options.unicode
89
+ * Whether to return the Unicode symbol.
90
+ * @returns {string}
91
+ * Display symbol for the requested symbol mode.
92
+ */
93
+ export function getResultSymbol(resultType, options = {}) {
94
+ const token = getResultToken(resultType);
95
+ const usesUnicode = options.unicode ?? true;
96
+
97
+ return usesUnicode ? token.symbols.unicode : token.symbols.ascii;
98
+ }