@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
package/src/index.js ADDED
@@ -0,0 +1,52 @@
1
+ export { createCliStyle, profiles } from "./create-cli-style.js";
2
+
3
+ export { resolveProfile } from "./capability/resolve-profile.js";
4
+ export { resolveTerminalCapabilities } from "./capability/terminal.js";
5
+ export { isProfile } from "./profiles/profiles.js";
6
+
7
+ export { renderHelp } from "./cli/help.js";
8
+ export {
9
+ parseRenderRequest,
10
+ rendererNames,
11
+ renderJsonInput,
12
+ } from "./cli/render-command.js";
13
+ export {
14
+ galleryFixtures,
15
+ gallerySections,
16
+ galleryVariants,
17
+ renderGallery,
18
+ } from "./gallery/render-gallery.js";
19
+
20
+ export { background, foreground, stripAnsi, style } from "./formatters/ansi.js";
21
+ export { agentTranscript } from "./patterns/agent-transcript.js";
22
+ export { auditFinding } from "./patterns/audit-finding.js";
23
+ export { commandResult } from "./patterns/command-result.js";
24
+ export { compactDataTable } from "./patterns/compact-data-table.js";
25
+ export { confirmationResult } from "./patterns/confirmation-result.js";
26
+ export { diagnosticReport } from "./patterns/diagnostic-report.js";
27
+ export { nextStepBlock } from "./patterns/next-step-block.js";
28
+ export { taskSummary } from "./patterns/task-summary.js";
29
+ export { barChart } from "./primitives/bar-chart.js";
30
+ export { chip } from "./primitives/chip.js";
31
+ export { divider } from "./primitives/divider.js";
32
+ export { emptyState, errorBlock, hint } from "./primitives/feedback.js";
33
+ export { panel } from "./primitives/panel.js";
34
+ export { progressBar } from "./primitives/progress-bar.js";
35
+ export { row } from "./primitives/row.js";
36
+ export { status } from "./primitives/status.js";
37
+ export { step, stepProgress, stepStates } from "./primitives/step-progress.js";
38
+ export { table } from "./primitives/table.js";
39
+ export {
40
+ chartColours,
41
+ chipColours,
42
+ colourTokens,
43
+ getColourToken,
44
+ getToneColour,
45
+ panelColours,
46
+ promptColours,
47
+ resolveColourValue,
48
+ tableColours,
49
+ terminalColours,
50
+ toneColours,
51
+ } from "./theme/colours.js";
52
+ export { getResultSymbol, getResultToken, resultTokens, resultTypes } from "./theme/results.js";
@@ -0,0 +1,180 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+ import { chip } from "../primitives/chip.js";
3
+ import {
4
+ isNonEmptyString,
5
+ isRecord,
6
+ normaliseStringList,
7
+ renderTitle,
8
+ } from "./helpers.js";
9
+
10
+ // Transcript roles map machine-facing values to stable textual labels and tones.
11
+ const transcriptRoles = {
12
+ agent: {
13
+ label: "Agent",
14
+ tone: "agent",
15
+ },
16
+ system: {
17
+ label: "System",
18
+ tone: "neutral",
19
+ },
20
+ tool: {
21
+ label: "Tool",
22
+ tone: "info",
23
+ },
24
+ user: {
25
+ label: "User",
26
+ tone: "user",
27
+ },
28
+ };
29
+
30
+ // Unsupported roles remain visible without inventing new semantics.
31
+ const fallbackRole = {
32
+ label: "Participant",
33
+ tone: "neutral",
34
+ };
35
+
36
+ // Agent transcripts use a stable title when the caller does not provide one.
37
+ const defaultTitle = "Agent transcript";
38
+
39
+ /**
40
+ * Render role-labelled agent exchanges and tool output.
41
+ *
42
+ * @param {object} transcript
43
+ * Structured transcript.
44
+ * @param {object[]} transcript.entries
45
+ * Role-labelled transcript entries.
46
+ * @param {string} transcript.title
47
+ * Transcript title.
48
+ * @param {object} options
49
+ * Rendering options.
50
+ * @returns {object|string}
51
+ * Structured JSON input or rendered transcript.
52
+ */
53
+ export function agentTranscript(transcript, options = {}) {
54
+ if (options.profile === profiles.JSON) {
55
+ return transcript;
56
+ }
57
+
58
+ if (!isRecord(transcript)) {
59
+ return "";
60
+ }
61
+
62
+ const entries = normaliseEntries(transcript.entries);
63
+ const title = isNonEmptyString(transcript.title) ? transcript.title : defaultTitle;
64
+
65
+ if (entries.length === 0) {
66
+ return renderTitle(title, options);
67
+ }
68
+
69
+ return [
70
+ renderTitle(title, options),
71
+ ...entries.map((entry) => renderEntry(entry, options)),
72
+ ].join("\n\n");
73
+ }
74
+
75
+ /**
76
+ * Return transcript entries with usable content.
77
+ *
78
+ * @param {*} entries
79
+ * Entries to normalise.
80
+ * @returns {object[]}
81
+ * Valid transcript entries.
82
+ */
83
+ function normaliseEntries(entries) {
84
+ if (!Array.isArray(entries)) {
85
+ return [];
86
+ }
87
+
88
+ return entries
89
+ .filter(isRecord)
90
+ .map((entry) => ({
91
+ ...entry,
92
+ content: normaliseContent(entry.content),
93
+ }))
94
+ .filter((entry) => entry.content.length > 0);
95
+ }
96
+
97
+ /**
98
+ * Return transcript content as non-empty lines.
99
+ *
100
+ * @param {*} content
101
+ * Entry content.
102
+ * @returns {string[]}
103
+ * Content lines.
104
+ */
105
+ function normaliseContent(content) {
106
+ if (isNonEmptyString(content)) {
107
+ return content.split("\n").filter(isNonEmptyString);
108
+ }
109
+
110
+ return normaliseStringList(content);
111
+ }
112
+
113
+ /**
114
+ * Render one transcript entry for the active profile.
115
+ *
116
+ * @param {object} entry
117
+ * Transcript entry.
118
+ * @param {object} options
119
+ * Rendering options.
120
+ * @returns {string}
121
+ * Rendered transcript entry.
122
+ */
123
+ function renderEntry(entry, options) {
124
+ const role = transcriptRoles[entry.role] ?? fallbackRole;
125
+ const label = formatRoleLabel(role.label, entry.name);
126
+
127
+ if (options.profile === profiles.AGENT) {
128
+ return renderAgentEntry(label, entry.content, entry.role);
129
+ }
130
+
131
+ const renderedLabel = chip(label, role.tone, options);
132
+
133
+ if (entry.content.length === 1) {
134
+ return `${renderedLabel} ${entry.content[0]}`;
135
+ }
136
+
137
+ return [
138
+ renderedLabel,
139
+ ...entry.content.map((line) => ` ${line}`),
140
+ ].join("\n");
141
+ }
142
+
143
+ /**
144
+ * Render one Markdown transcript entry.
145
+ *
146
+ * @param {string} label
147
+ * Entry role label.
148
+ * @param {string[]} content
149
+ * Entry content lines.
150
+ * @param {string} role
151
+ * Machine-facing entry role.
152
+ * @returns {string}
153
+ * Markdown transcript entry.
154
+ */
155
+ function renderAgentEntry(label, content, role) {
156
+ if (role === "tool") {
157
+ return [
158
+ `## ${label}`,
159
+ "```text",
160
+ ...content,
161
+ "```",
162
+ ].join("\n");
163
+ }
164
+
165
+ return [`## ${label}`, ...content].join("\n");
166
+ }
167
+
168
+ /**
169
+ * Add a source name to a role label when provided.
170
+ *
171
+ * @param {string} label
172
+ * Base role label.
173
+ * @param {*} name
174
+ * Optional source name.
175
+ * @returns {string}
176
+ * Complete role label.
177
+ */
178
+ function formatRoleLabel(label, name) {
179
+ return isNonEmptyString(name) ? `${label}: ${name}` : label;
180
+ }
@@ -0,0 +1,107 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+ import { row } from "../primitives/row.js";
3
+ import { status } from "../primitives/status.js";
4
+ import { resultTypes } from "../theme/results.js";
5
+ import {
6
+ isNonEmptyString,
7
+ isRecord,
8
+ normaliseStringList,
9
+ renderSection,
10
+ renderTitle,
11
+ } from "./helpers.js";
12
+
13
+ // Audit findings use a stable title when the caller does not provide one.
14
+ const defaultTitle = "Audit finding";
15
+
16
+ /**
17
+ * Render one audit finding with evidence and remediation guidance.
18
+ *
19
+ * @param {object} finding
20
+ * Structured audit finding.
21
+ * @param {string[]} finding.evidence
22
+ * Evidence supporting the finding.
23
+ * @param {string} finding.finding
24
+ * Finding summary.
25
+ * @param {string} finding.location
26
+ * Source location or affected area.
27
+ * @param {string} finding.recommendation
28
+ * Recommended remediation.
29
+ * @param {string[]} finding.references
30
+ * Supporting standards or documentation.
31
+ * @param {string} finding.result
32
+ * Semantic severity state.
33
+ * @param {string} finding.title
34
+ * Pattern title.
35
+ * @param {object} options
36
+ * Rendering options.
37
+ * @returns {object|string}
38
+ * Structured JSON input or rendered audit finding.
39
+ */
40
+ export function auditFinding(finding, options = {}) {
41
+ if (options.profile === profiles.JSON) {
42
+ return finding;
43
+ }
44
+
45
+ if (!isRecord(finding)) {
46
+ return "";
47
+ }
48
+
49
+ const title = isNonEmptyString(finding.title) ? finding.title : defaultTitle;
50
+ const summary = isNonEmptyString(finding.finding) ? finding.finding : "";
51
+ const evidence = normaliseStringList(finding.evidence).map((item) => `- ${item}`);
52
+ const references = normaliseStringList(finding.references).map((item) => `- ${item}`);
53
+ const sections = [
54
+ status(finding.result ?? resultTypes.UNKNOWN, summary, options),
55
+ renderLocation(finding.location, options),
56
+ renderSection("Evidence", evidence, options, false),
57
+ renderTextSection("Recommendation", finding.recommendation, options),
58
+ renderSection("References", references, options, false),
59
+ ].filter((section) => section !== "");
60
+
61
+ return [renderTitle(title, options), ...sections].join("\n\n");
62
+ }
63
+
64
+ /**
65
+ * Render the finding location for the active profile.
66
+ *
67
+ * @param {*} location
68
+ * Finding location.
69
+ * @param {object} options
70
+ * Rendering options.
71
+ * @returns {string}
72
+ * Rendered location.
73
+ */
74
+ function renderLocation(location, options) {
75
+ if (!isNonEmptyString(location)) {
76
+ return "";
77
+ }
78
+
79
+ if (options.profile === profiles.AGENT) {
80
+ return renderSection("Location", [location], options, false);
81
+ }
82
+
83
+ return row("Location", location, {
84
+ ...options,
85
+ labelWidth: 8,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Render a section containing one text value.
91
+ *
92
+ * @param {string} title
93
+ * Section title.
94
+ * @param {*} value
95
+ * Section content.
96
+ * @param {object} options
97
+ * Rendering options.
98
+ * @returns {string}
99
+ * Rendered text section.
100
+ */
101
+ function renderTextSection(title, value, options) {
102
+ if (!isNonEmptyString(value)) {
103
+ return "";
104
+ }
105
+
106
+ return renderSection(title, [value], options, false);
107
+ }
@@ -0,0 +1,91 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+ import { row } from "../primitives/row.js";
3
+ import { status } from "../primitives/status.js";
4
+ import { resultTypes } from "../theme/results.js";
5
+ import {
6
+ isNonEmptyString,
7
+ isRecord,
8
+ normaliseStringList,
9
+ renderSection,
10
+ renderTitle,
11
+ } from "./helpers.js";
12
+
13
+ // Command results use a stable title when the caller does not provide one.
14
+ const defaultTitle = "Command result";
15
+
16
+ /**
17
+ * Render one command outcome with metadata and supporting details.
18
+ *
19
+ * @param {object} result
20
+ * Structured command result.
21
+ * @param {string} result.command
22
+ * Executed command.
23
+ * @param {string[]} result.details
24
+ * Supporting result details.
25
+ * @param {string} result.duration
26
+ * Human-readable command duration.
27
+ * @param {number} result.exitCode
28
+ * Command exit code.
29
+ * @param {string} result.result
30
+ * Semantic result state.
31
+ * @param {string} result.summary
32
+ * Short outcome summary.
33
+ * @param {string} result.title
34
+ * Pattern title.
35
+ * @param {object} options
36
+ * Rendering options.
37
+ * @returns {object|string}
38
+ * Structured JSON input or rendered command result.
39
+ */
40
+ export function commandResult(result, options = {}) {
41
+ if (options.profile === profiles.JSON) {
42
+ return result;
43
+ }
44
+
45
+ if (!isRecord(result)) {
46
+ return "";
47
+ }
48
+
49
+ const title = isNonEmptyString(result.title) ? result.title : defaultTitle;
50
+ const metadata = renderMetadata(result, options);
51
+ const details = normaliseStringList(result.details).map((detail) => `- ${detail}`);
52
+ const summary = isNonEmptyString(result.summary) ? result.summary : "";
53
+ const sections = [
54
+ status(result.result ?? resultTypes.UNKNOWN, summary, options),
55
+ metadata,
56
+ renderSection("Details", details, options, false),
57
+ ].filter((section) => section !== "");
58
+
59
+ return [renderTitle(title, options), ...sections].join("\n\n");
60
+ }
61
+
62
+ /**
63
+ * Render command metadata for human or agent output.
64
+ *
65
+ * @param {object} result
66
+ * Structured command result.
67
+ * @param {object} options
68
+ * Rendering options.
69
+ * @returns {string}
70
+ * Rendered metadata.
71
+ */
72
+ function renderMetadata(result, options) {
73
+ const entries = [
74
+ isNonEmptyString(result.command) ? ["Command", result.command] : null,
75
+ Number.isInteger(result.exitCode) ? ["Exit code", String(result.exitCode)] : null,
76
+ isNonEmptyString(result.duration) ? ["Duration", result.duration] : null,
77
+ ].filter((entry) => entry !== null);
78
+
79
+ if (options.profile === profiles.AGENT) {
80
+ const lines = entries.map(([label, value]) => `- ${label}: ${value}`);
81
+
82
+ return renderSection("Metadata", lines, options, false);
83
+ }
84
+
85
+ return entries
86
+ .map(([label, value]) => row(label, value, {
87
+ ...options,
88
+ labelWidth: 9,
89
+ }))
90
+ .join("\n");
91
+ }
@@ -0,0 +1,67 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+ import { table } from "../primitives/table.js";
3
+ import {
4
+ isNonEmptyString,
5
+ isRecord,
6
+ renderTitle,
7
+ } from "./helpers.js";
8
+
9
+ // Compact data tables use a stable title when the caller does not provide one.
10
+ const defaultTitle = "Data";
11
+
12
+ /**
13
+ * Render dense records with optional summary context.
14
+ *
15
+ * @param {object} data
16
+ * Structured table data.
17
+ * @param {object[]} data.columns
18
+ * Table column keys and labels.
19
+ * @param {object[]} data.rows
20
+ * Table records.
21
+ * @param {string} data.summary
22
+ * Optional summary context.
23
+ * @param {string} data.title
24
+ * Pattern title.
25
+ * @param {object} options
26
+ * Rendering options.
27
+ * @returns {object|string}
28
+ * Structured JSON input or rendered compact table.
29
+ */
30
+ export function compactDataTable(data, options = {}) {
31
+ if (options.profile === profiles.JSON) {
32
+ return data;
33
+ }
34
+
35
+ if (!isRecord(data) || !hasEntries(data.columns) || !hasEntries(data.rows)) {
36
+ return "";
37
+ }
38
+
39
+ const title = isNonEmptyString(data.title) ? data.title : defaultTitle;
40
+ const summary = isNonEmptyString(data.summary) ? data.summary : "";
41
+ const renderedTable = table({
42
+ ...options,
43
+ columns: data.columns,
44
+ rows: data.rows,
45
+ });
46
+ const heading = summary === ""
47
+ ? renderTitle(title, options)
48
+ : [renderTitle(title, options), summary].join("\n");
49
+
50
+ if (renderedTable === "") {
51
+ return "";
52
+ }
53
+
54
+ return [heading, renderedTable].join("\n\n");
55
+ }
56
+
57
+ /**
58
+ * Check whether a value is a non-empty array.
59
+ *
60
+ * @param {*} value
61
+ * Value to check.
62
+ * @returns {boolean}
63
+ * Whether the value contains entries.
64
+ */
65
+ function hasEntries(value) {
66
+ return Array.isArray(value) && value.length > 0;
67
+ }
@@ -0,0 +1,88 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+ import { status } from "../primitives/status.js";
3
+ import { resultTypes } from "../theme/results.js";
4
+ import {
5
+ formatLabel,
6
+ isNonEmptyString,
7
+ isRecord,
8
+ renderSection,
9
+ renderTitle,
10
+ } from "./helpers.js";
11
+
12
+ // Confirmation states map workflow outcomes onto shared result semantics.
13
+ const confirmationStates = {
14
+ cancelled: {
15
+ label: "Cancelled",
16
+ result: resultTypes.SKIPPED,
17
+ },
18
+ confirmed: {
19
+ label: "Confirmed",
20
+ result: resultTypes.SUCCESS,
21
+ },
22
+ declined: {
23
+ label: "Declined",
24
+ result: resultTypes.WARNING,
25
+ },
26
+ failed: {
27
+ label: "Failed",
28
+ result: resultTypes.FAILED,
29
+ },
30
+ };
31
+
32
+ // Unsupported confirmation states remain visibly unknown.
33
+ const fallbackState = {
34
+ label: "Unknown",
35
+ result: resultTypes.UNKNOWN,
36
+ };
37
+
38
+ // Confirmation results use a stable title when the caller does not provide one.
39
+ const defaultTitle = "Confirmation result";
40
+
41
+ /**
42
+ * Render one confirmation workflow outcome.
43
+ *
44
+ * @param {object} confirmation
45
+ * Structured confirmation result.
46
+ * @param {string} confirmation.action
47
+ * Action requested or completed.
48
+ * @param {string} confirmation.detail
49
+ * Supporting outcome detail.
50
+ * @param {string} confirmation.item
51
+ * Affected item.
52
+ * @param {string} confirmation.state
53
+ * Confirmation state.
54
+ * @param {string} confirmation.title
55
+ * Pattern title.
56
+ * @param {object} options
57
+ * Rendering options.
58
+ * @returns {object|string}
59
+ * Structured JSON input or rendered confirmation result.
60
+ */
61
+ export function confirmationResult(confirmation, options = {}) {
62
+ if (options.profile === profiles.JSON) {
63
+ return confirmation;
64
+ }
65
+
66
+ if (!isRecord(confirmation)) {
67
+ return "";
68
+ }
69
+
70
+ const title = isNonEmptyString(confirmation.title)
71
+ ? confirmation.title
72
+ : defaultTitle;
73
+ const action = isNonEmptyString(confirmation.action) ? confirmation.action : "";
74
+ const summary = formatLabel(action, confirmation.item, options);
75
+ const state = confirmationStates[confirmation.state] ?? fallbackState;
76
+ const detail = isNonEmptyString(confirmation.detail)
77
+ ? renderSection("Detail", [confirmation.detail], options, false)
78
+ : "";
79
+ const sections = [
80
+ status(state.result, summary, {
81
+ ...options,
82
+ label: state.label,
83
+ }),
84
+ detail,
85
+ ].filter((section) => section !== "");
86
+
87
+ return [renderTitle(title, options), ...sections].join("\n\n");
88
+ }