@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,83 @@
1
+ import { foreground, style } from "../formatters/ansi.js";
2
+ import { getResultSymbol, resultTypes } from "../theme/results.js";
3
+ import { panel } from "./panel.js";
4
+
5
+ /**
6
+ * Render an informational hint.
7
+ *
8
+ * @param {string} message
9
+ * Hint text.
10
+ * @param {object} options
11
+ * Rendering options.
12
+ * @param {boolean} options.colour
13
+ * Whether ANSI colour should be applied.
14
+ * @returns {string}
15
+ * Rendered hint.
16
+ */
17
+ export function hint(message = "", options = {}) {
18
+ const prefix = "i Hint:";
19
+
20
+ if (options.colour !== true) {
21
+ return `${prefix} ${message}`;
22
+ }
23
+
24
+ const renderedPrefix = style(foreground(prefix, "info", options), "bold", options);
25
+
26
+ return `${renderedPrefix} ${message}`;
27
+ }
28
+
29
+ /**
30
+ * Render an empty-state message.
31
+ *
32
+ * @param {string} title
33
+ * Empty-state title.
34
+ * @param {string} detail
35
+ * Optional supporting detail.
36
+ * @param {object} options
37
+ * Rendering options.
38
+ * @param {boolean} options.colour
39
+ * Whether ANSI colour should be applied.
40
+ * @param {boolean} options.unicode
41
+ * Whether Unicode symbols should be used.
42
+ * @returns {string}
43
+ * Rendered empty state.
44
+ */
45
+ export function emptyState(title = "No results", detail = "", options = {}) {
46
+ const symbol = getResultSymbol(resultTypes.SKIPPED, options);
47
+ const prefix = `${symbol} ${title}`;
48
+ const detailText = detail === "" ? "" : ` ${detail}`;
49
+
50
+ if (options.colour !== true) {
51
+ return `${prefix}${detailText}`;
52
+ }
53
+
54
+ const renderedPrefix = style(foreground(prefix, "muted", options), "bold", options);
55
+
56
+ return `${renderedPrefix}${detailText}`;
57
+ }
58
+
59
+ /**
60
+ * Render a structured error block.
61
+ *
62
+ * @param {string} title
63
+ * Error title.
64
+ * @param {string[]} lines
65
+ * Supporting error details.
66
+ * @param {object} options
67
+ * Rendering options.
68
+ * @param {boolean} options.unicode
69
+ * Whether Unicode symbols should be used.
70
+ * @returns {string}
71
+ * Rendered error block.
72
+ */
73
+ export function errorBlock(title = "Failed", lines = [], options = {}) {
74
+ const symbol = getResultSymbol(resultTypes.FAILED, options);
75
+ const detailLines = Array.isArray(lines) ? lines : [];
76
+
77
+ return panel({
78
+ ...options,
79
+ lines: detailLines,
80
+ title: `${symbol} Error: ${title}`,
81
+ tone: "danger",
82
+ });
83
+ }
@@ -0,0 +1,100 @@
1
+ import { background, foreground, style } from "../formatters/ansi.js";
2
+ import { profiles } from "../profiles/profiles.js";
3
+ import { panelColours } from "../theme/colours.js";
4
+
5
+ // Panels use a stable width so grouped output aligns across a report.
6
+ const defaultWidth = 40;
7
+
8
+ // Info is the neutral semantic accent for grouped explanatory content.
9
+ const defaultTone = "info";
10
+
11
+ /**
12
+ * Render a grouped block with a semantic left accent.
13
+ *
14
+ * @param {object} options
15
+ * Rendering options.
16
+ * @param {boolean} options.colour
17
+ * Whether ANSI colour should be applied.
18
+ * @param {string[]} options.lines
19
+ * Content lines to render.
20
+ * @param {number} options.panelWidth
21
+ * Total panel width.
22
+ * @param {string} options.profile
23
+ * Active output profile.
24
+ * @param {string} options.title
25
+ * Optional panel title.
26
+ * @param {string} options.tone
27
+ * Semantic accent tone.
28
+ * @param {boolean} options.unicode
29
+ * Whether Unicode symbols should be used.
30
+ * @returns {string}
31
+ * Rendered panel.
32
+ */
33
+ export function panel(options = {}) {
34
+ const lines = options.lines ?? [];
35
+
36
+ if (options.profile === profiles.CI) {
37
+ return [
38
+ ...(options.title === undefined ? [] : [options.title]),
39
+ ...lines,
40
+ ].join("\n");
41
+ }
42
+
43
+ const width = Math.max(options.panelWidth ?? defaultWidth, 4);
44
+ const panelLines = [
45
+ "",
46
+ ...(options.title === undefined ? [] : [options.title]),
47
+ ...lines,
48
+ "",
49
+ ];
50
+
51
+ return panelLines
52
+ .map((line, index) => renderLine(line, width, index === 1 && options.title !== undefined, options))
53
+ .join("\n");
54
+ }
55
+
56
+ /**
57
+ * Render one fixed-width panel line.
58
+ *
59
+ * @param {string} line
60
+ * Content line.
61
+ * @param {number} width
62
+ * Total panel width.
63
+ * @param {boolean} isTitle
64
+ * Whether the line is the panel title.
65
+ * @param {object} options
66
+ * Rendering options.
67
+ * @returns {string}
68
+ * Rendered content line.
69
+ */
70
+ function renderLine(line, width, isTitle, options) {
71
+ const accent = options.unicode === false ? "|" : "▌";
72
+ const contentWidth = width - 5;
73
+ const visibleLine = line.slice(0, contentWidth);
74
+ const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
75
+
76
+ if (options.colour !== true) {
77
+ return `${accent}${content}`;
78
+ }
79
+
80
+ const renderedAccent = renderPanelColour(accent, options);
81
+ const renderedContent = background(content, panelColours.background, options);
82
+
83
+ return `${renderedAccent}${isTitle ? style(renderedContent, "bold", options) : renderedContent}`;
84
+ }
85
+
86
+ /**
87
+ * Apply the panel's semantic colour.
88
+ *
89
+ * @param {string} value
90
+ * Panel text to colour.
91
+ * @param {object} options
92
+ * Rendering options.
93
+ * @returns {string}
94
+ * Coloured panel text.
95
+ */
96
+ function renderPanelColour(value, options) {
97
+ const tone = panelColours[options.tone ?? defaultTone] ?? panelColours[defaultTone];
98
+
99
+ return foreground(value, tone, options);
100
+ }
@@ -0,0 +1,121 @@
1
+ import { foreground } from "../formatters/ansi.js";
2
+ import { getToneColour } from "../theme/colours.js";
3
+
4
+ // Progress bars balance useful resolution with compact terminal output.
5
+ const defaultWidth = 20;
6
+
7
+ // Success is the default tone for work progressing normally.
8
+ const defaultTone = "success";
9
+
10
+ // Invalid maxima fall back to a percentage-friendly range.
11
+ const defaultMaximum = 100;
12
+
13
+ /**
14
+ * Render a single value against a maximum.
15
+ *
16
+ * @param {object} options
17
+ * Rendering options.
18
+ * @param {number} options.barWidth
19
+ * Number of characters used by the bar.
20
+ * @param {boolean} options.colour
21
+ * Whether ANSI colour should be applied.
22
+ * @param {number} options.max
23
+ * Maximum progress value.
24
+ * @param {string} options.tone
25
+ * Semantic colour tone for completed progress.
26
+ * @param {boolean} options.unicode
27
+ * Whether Unicode bar characters should be used.
28
+ * @param {number} options.value
29
+ * Current progress value.
30
+ * @returns {string}
31
+ * Rendered progress bar.
32
+ */
33
+ export function progressBar(options = {}) {
34
+ const maximum = normaliseMaximum(options.max);
35
+ const value = normaliseValue(options.value);
36
+ const width = normaliseWidth(options.barWidth);
37
+ const ratio = Math.min(Math.max(value / maximum, 0), 1);
38
+ const filledWidth = Math.round(width * ratio);
39
+ const emptyWidth = width - filledWidth;
40
+ const symbols = barSymbols(options);
41
+ const filled = symbols.filled.repeat(filledWidth);
42
+ const empty = symbols.empty.repeat(emptyWidth);
43
+ const percentage = Math.round(ratio * 100);
44
+ const numericText = `${percentage}% (${value}/${maximum})`;
45
+
46
+ if (options.colour !== true) {
47
+ return `[${filled}${empty}] ${numericText}`;
48
+ }
49
+
50
+ const tone = getToneColour(options.tone) === null ? defaultTone : options.tone;
51
+ const renderedFilled = filled === "" ? "" : foreground(filled, tone ?? defaultTone, options);
52
+ const renderedEmpty = empty === "" ? "" : foreground(empty, "muted", options);
53
+
54
+ return `[${renderedFilled}${renderedEmpty}] ${numericText}`;
55
+ }
56
+
57
+ /**
58
+ * Return progress characters for the active capability.
59
+ *
60
+ * @param {object} options
61
+ * Rendering options.
62
+ * @returns {object}
63
+ * Filled and empty progress symbols.
64
+ */
65
+ function barSymbols(options) {
66
+ if (options.unicode === false) {
67
+ return {
68
+ empty: "-",
69
+ filled: "#",
70
+ };
71
+ }
72
+
73
+ return {
74
+ empty: "░",
75
+ filled: "█",
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Return a usable maximum.
81
+ *
82
+ * @param {number} maximum
83
+ * Requested maximum.
84
+ * @returns {number}
85
+ * Positive finite maximum.
86
+ */
87
+ function normaliseMaximum(maximum) {
88
+ if (!Number.isFinite(maximum) || maximum <= 0) {
89
+ return defaultMaximum;
90
+ }
91
+
92
+ return maximum;
93
+ }
94
+
95
+ /**
96
+ * Return a usable progress value.
97
+ *
98
+ * @param {number} value
99
+ * Requested progress value.
100
+ * @returns {number}
101
+ * Finite progress value.
102
+ */
103
+ function normaliseValue(value) {
104
+ return Number.isFinite(value) ? value : 0;
105
+ }
106
+
107
+ /**
108
+ * Return a usable integer bar width.
109
+ *
110
+ * @param {number} width
111
+ * Requested bar width.
112
+ * @returns {number}
113
+ * Positive integer width.
114
+ */
115
+ function normaliseWidth(width) {
116
+ if (!Number.isFinite(width)) {
117
+ return defaultWidth;
118
+ }
119
+
120
+ return Math.max(Math.floor(width), 1);
121
+ }
@@ -0,0 +1,80 @@
1
+ import { foreground } from "../formatters/ansi.js";
2
+
3
+ // Row labels use the same subdued tone as table headers by default.
4
+ const defaultLabelColour = "muted";
5
+
6
+ // Rows use a compact gap that still reads cleanly in plain terminals.
7
+ const defaultSeparator = " ";
8
+
9
+ /**
10
+ * Render a labelled value row.
11
+ *
12
+ * @param {string} label
13
+ * Label text to render.
14
+ * @param {string} value
15
+ * Value text to render.
16
+ * @param {object} options
17
+ * Rendering options.
18
+ * @param {boolean} options.colour
19
+ * Whether ANSI colour should be applied.
20
+ * @param {string} options.labelColour
21
+ * Colour token for the label.
22
+ * @param {number} options.labelWidth
23
+ * Minimum label width before the separator.
24
+ * @param {string} options.separator
25
+ * Text between label and value.
26
+ * @param {string} options.valueColour
27
+ * Optional colour token for the value.
28
+ * @returns {string}
29
+ * Rendered row.
30
+ */
31
+ export function row(label, value = "", options = {}) {
32
+ const labelWidth = Math.max(label.length, options.labelWidth ?? 0);
33
+ const labelText = label.padEnd(labelWidth, " ");
34
+ const renderedLabel = renderLabel(labelText, options);
35
+
36
+ if (value === "") {
37
+ return renderedLabel;
38
+ }
39
+
40
+ const renderedValue = renderValue(value, options);
41
+ const separator = options.separator ?? defaultSeparator;
42
+
43
+ return `${renderedLabel}${separator}${renderedValue}`;
44
+ }
45
+
46
+ /**
47
+ * Render a row label.
48
+ *
49
+ * @param {string} label
50
+ * Label text to render.
51
+ * @param {object} options
52
+ * Rendering options.
53
+ * @returns {string}
54
+ * Rendered label.
55
+ */
56
+ function renderLabel(label, options) {
57
+ if (options.colour !== true) {
58
+ return label;
59
+ }
60
+
61
+ return foreground(label, options.labelColour ?? defaultLabelColour, options);
62
+ }
63
+
64
+ /**
65
+ * Render a row value.
66
+ *
67
+ * @param {string} value
68
+ * Value text to render.
69
+ * @param {object} options
70
+ * Rendering options.
71
+ * @returns {string}
72
+ * Rendered value.
73
+ */
74
+ function renderValue(value, options) {
75
+ if (options.colour !== true || options.valueColour === undefined) {
76
+ return value;
77
+ }
78
+
79
+ return foreground(value, options.valueColour, options);
80
+ }
@@ -0,0 +1,39 @@
1
+ import { foreground, style } from "../formatters/ansi.js";
2
+ import { getResultSymbol, getResultToken, resultTypes } from "../theme/results.js";
3
+
4
+ /**
5
+ * Render a result status line.
6
+ *
7
+ * @param {string} resultType
8
+ * Result type to render.
9
+ * @param {string} detail
10
+ * Optional detail text.
11
+ * @param {object} options
12
+ * Rendering options.
13
+ * @param {boolean} options.colour
14
+ * Whether ANSI colour should be applied.
15
+ * @param {string} options.label
16
+ * Optional text label override.
17
+ * @param {boolean} options.unicode
18
+ * Whether Unicode symbols should be used.
19
+ * @returns {string}
20
+ * Rendered status line.
21
+ */
22
+ export function status(resultType = resultTypes.UNKNOWN, detail = "", options = {}) {
23
+ const token = getResultToken(resultType);
24
+ const symbol = getResultSymbol(resultType, options);
25
+ const label = typeof options.label === "string" && options.label !== ""
26
+ ? options.label
27
+ : token.label;
28
+ const prefix = `${symbol} ${label}`;
29
+ const detailText = detail === "" ? "" : ` ${detail}`;
30
+
31
+ if (options.colour !== true) {
32
+ return `${prefix}${detailText}`;
33
+ }
34
+
35
+ const colouredPrefix = foreground(prefix, token.tone, options);
36
+ const boldPrefix = style(colouredPrefix, "bold", options);
37
+
38
+ return `${boldPrefix}${detailText}`;
39
+ }
@@ -0,0 +1,184 @@
1
+ import { foreground } from "../formatters/ansi.js";
2
+ import { getResultSymbol, getResultToken, resultTypes } from "../theme/results.js";
3
+
4
+ // Step states describe position in an ordered flow.
5
+ export const stepStates = {
6
+ COMPLETE: "complete",
7
+ CURRENT: "current",
8
+ PENDING: "pending",
9
+ };
10
+
11
+ // Current steps use the running symbol defined by the package contract.
12
+ const currentToken = {
13
+ symbols: {
14
+ ascii: "...",
15
+ unicode: "…",
16
+ },
17
+ tone: "info",
18
+ };
19
+
20
+ /**
21
+ * Render one labelled step state.
22
+ *
23
+ * @param {string} label
24
+ * Step label.
25
+ * @param {string} state
26
+ * Step state or result type.
27
+ * @param {object} options
28
+ * Rendering options.
29
+ * @param {boolean} options.colour
30
+ * Whether ANSI colour should be applied.
31
+ * @param {boolean} options.unicode
32
+ * Whether Unicode symbols should be used.
33
+ * @returns {string}
34
+ * Rendered step.
35
+ */
36
+ export function step(label = "", state = stepStates.PENDING, options = {}) {
37
+ const token = stepToken(state);
38
+ const symbol = stepSymbol(state, token, options);
39
+
40
+ if (options.colour !== true) {
41
+ return `${symbol} ${label}`;
42
+ }
43
+
44
+ const renderedSymbol = foreground(symbol, token.tone, options);
45
+
46
+ return `${renderedSymbol} ${label}`;
47
+ }
48
+
49
+ /**
50
+ * Render an ordered sequence with inferred states and explicit counts.
51
+ *
52
+ * @param {object} options
53
+ * Rendering options.
54
+ * @param {number} options.current
55
+ * Zero-based current step index.
56
+ * @param {string[]} options.steps
57
+ * Ordered step labels.
58
+ * @returns {string}
59
+ * Rendered step progress.
60
+ */
61
+ export function stepProgress(options = {}) {
62
+ const steps = normaliseSteps(options.steps);
63
+
64
+ if (steps.length === 0) {
65
+ return "";
66
+ }
67
+
68
+ const current = normaliseCurrent(options.current, steps.length);
69
+
70
+ return steps.map((label, index) => {
71
+ const state = stateForIndex(index, current);
72
+ const count = `${index + 1}/${steps.length}`;
73
+
74
+ return `${count} ${step(label, state, options)}`;
75
+ }).join("\n");
76
+ }
77
+
78
+ /**
79
+ * Return valid step labels.
80
+ *
81
+ * @param {string[]} steps
82
+ * Requested step labels.
83
+ * @returns {string[]}
84
+ * Non-empty labels.
85
+ */
86
+ function normaliseSteps(steps) {
87
+ if (!Array.isArray(steps)) {
88
+ return [];
89
+ }
90
+
91
+ return steps.filter((label) => typeof label === "string" && label !== "");
92
+ }
93
+
94
+ /**
95
+ * Return a current index within the step range.
96
+ *
97
+ * @param {number} current
98
+ * Requested current index.
99
+ * @param {number} stepCount
100
+ * Number of available steps.
101
+ * @returns {number}
102
+ * Clamped current index.
103
+ */
104
+ function normaliseCurrent(current, stepCount) {
105
+ if (!Number.isFinite(current)) {
106
+ return 0;
107
+ }
108
+
109
+ return Math.min(Math.max(Math.floor(current), 0), stepCount - 1);
110
+ }
111
+
112
+ /**
113
+ * Infer a positional state for a step index.
114
+ *
115
+ * @param {number} index
116
+ * Step index.
117
+ * @param {number} current
118
+ * Current step index.
119
+ * @returns {string}
120
+ * Positional step state.
121
+ */
122
+ function stateForIndex(index, current) {
123
+ if (index < current) {
124
+ return stepStates.COMPLETE;
125
+ }
126
+
127
+ if (index === current) {
128
+ return stepStates.CURRENT;
129
+ }
130
+
131
+ return stepStates.PENDING;
132
+ }
133
+
134
+ /**
135
+ * Return the display token for a step state.
136
+ *
137
+ * @param {string} state
138
+ * Step state or result type.
139
+ * @returns {object}
140
+ * Symbol and tone token.
141
+ */
142
+ function stepToken(state) {
143
+ if (state === stepStates.COMPLETE) {
144
+ return getResultToken(resultTypes.SUCCESS);
145
+ }
146
+
147
+ if (state === stepStates.CURRENT) {
148
+ return currentToken;
149
+ }
150
+
151
+ if (state === stepStates.PENDING) {
152
+ return getResultToken(resultTypes.SKIPPED);
153
+ }
154
+
155
+ return getResultToken(state);
156
+ }
157
+
158
+ /**
159
+ * Return the display symbol for a step state.
160
+ *
161
+ * @param {string} state
162
+ * Step state or result type.
163
+ * @param {object} token
164
+ * Resolved step token.
165
+ * @param {object} options
166
+ * Rendering options.
167
+ * @returns {string}
168
+ * Step symbol.
169
+ */
170
+ function stepSymbol(state, token, options) {
171
+ if (state === stepStates.CURRENT) {
172
+ return options.unicode === false ? token.symbols.ascii : token.symbols.unicode;
173
+ }
174
+
175
+ if (state === stepStates.COMPLETE) {
176
+ return getResultSymbol(resultTypes.SUCCESS, options);
177
+ }
178
+
179
+ if (state === stepStates.PENDING) {
180
+ return getResultSymbol(resultTypes.SKIPPED, options);
181
+ }
182
+
183
+ return getResultSymbol(state, options);
184
+ }