@lewishowles/cli-style 0.13.0 → 0.15.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.
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- // Adapter paths returned for shell and Python wrapper setup.
5
+ // Adapter paths returned for consumer setup.
6
6
  const adapterPaths = {
7
7
  bash: {
8
8
  releasePath: "adapters/bash/cli-style.sh",
@@ -13,8 +13,8 @@ const adapterPaths = {
13
13
  sourceUrl: new URL("../../adapters/python", import.meta.url),
14
14
  },
15
15
  swift: {
16
- releasePath: "adapters/swift/CliStyle.swift",
17
- sourceUrl: new URL("../../adapters/swift/CliStyle.swift", import.meta.url),
16
+ releasePath: "adapters/swift",
17
+ sourceUrl: new URL("../../adapters/swift", import.meta.url),
18
18
  },
19
19
  };
20
20
 
@@ -8,6 +8,7 @@ import { diffBlock } from "../patterns/diff-block.js";
8
8
  import { nextStepBlock } from "../patterns/next-step-block.js";
9
9
  import { taskSummary } from "../patterns/task-summary.js";
10
10
  import { barChart } from "../primitives/bar-chart.js";
11
+ import { bulletList } from "../primitives/bullet-list.js";
11
12
  import { chip } from "../primitives/chip.js";
12
13
  import { divider } from "../primitives/divider.js";
13
14
  import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
@@ -33,6 +34,11 @@ const renderers = {
33
34
  ...input,
34
35
  ...options,
35
36
  }),
37
+ "bullet-list": (input, options) =>
38
+ bulletList(input.items, {
39
+ ...input,
40
+ ...options,
41
+ }),
36
42
  chip: (input, options) =>
37
43
  chip(input.label, input.tone, {
38
44
  ...input,
@@ -10,6 +10,7 @@ import { taskSummary } from "../patterns/task-summary.js";
10
10
  import { stripAnsi } from "../formatters/ansi.js";
11
11
  import { renderSpinnerFrame } from "../interactive/spinner.js";
12
12
  import { barChart } from "../primitives/bar-chart.js";
13
+ import { bulletList } from "../primitives/bullet-list.js";
13
14
  import { chip } from "../primitives/chip.js";
14
15
  import { divider } from "../primitives/divider.js";
15
16
  import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
@@ -776,6 +777,9 @@ function renderPrimitives(options) {
776
777
  width: 18,
777
778
  }),
778
779
  "",
780
+ "Bullet lists",
781
+ renderBulletListExamples(options),
782
+ "",
779
783
  "Feedback",
780
784
  hint(`Run ${span("npm run docs:readme", "info", options)}`, options),
781
785
  labelledLine("Notice", "Reusable labelled output", {
@@ -872,6 +876,42 @@ function renderPrimitives(options) {
872
876
  ].join("\n");
873
877
  }
874
878
 
879
+ /**
880
+ * Render representative flat, wrapped, nested, narrow, and plain bullet lists.
881
+ *
882
+ * @param {object} options
883
+ * Rendering options.
884
+ * @returns {string}
885
+ * Bullet list examples.
886
+ */
887
+ function renderBulletListExamples(options) {
888
+ return [
889
+ bulletList(["Install dependencies", "Run the unit tests", "Review the output"], options),
890
+ bulletList(["This item wraps so continuation lines stay aligned with the item text."], {
891
+ ...options,
892
+ width: 40,
893
+ }),
894
+ bulletList(
895
+ [
896
+ {
897
+ items: ["Use the focused check", "Read the first failure"],
898
+ text: "Verification",
899
+ },
900
+ "Commit the completed chunk",
901
+ ],
902
+ options,
903
+ ),
904
+ bulletList(["Narrow output keeps the gutter readable."], {
905
+ ...options,
906
+ width: 24,
907
+ }),
908
+ bulletList(["ASCII output remains easy to grep."], {
909
+ ...options,
910
+ profile: "plain",
911
+ }),
912
+ ].join("\n");
913
+ }
914
+
875
915
  /**
876
916
  * Render wide or narrow table diagnostics.
877
917
  *
package/src/index.js CHANGED
@@ -40,6 +40,7 @@ export { diffBlock } from "./patterns/diff-block.js";
40
40
  export { nextStepBlock } from "./patterns/next-step-block.js";
41
41
  export { taskSummary } from "./patterns/task-summary.js";
42
42
  export { barChart } from "./primitives/bar-chart.js";
43
+ export { bulletList } from "./primitives/bullet-list.js";
43
44
  export { chip } from "./primitives/chip.js";
44
45
  export { divider } from "./primitives/divider.js";
45
46
  export { emptyState, errorBlock, hint } from "./primitives/feedback.js";
@@ -62,6 +63,8 @@ export {
62
63
  renderSection,
63
64
  } from "./reporters/create-reporter.js";
64
65
 
66
+ export { bulletTokens, getBulletSymbol } from "./theme/bullets.js";
67
+
65
68
  export {
66
69
  chartColours,
67
70
  chipColours,
@@ -48,15 +48,15 @@ export function auditFinding(finding, options = {}) {
48
48
 
49
49
  const title = isNonEmptyString(finding.title) ? finding.title : defaultTitle;
50
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}`);
51
+ const evidence = normaliseStringList(finding.evidence);
52
+ const references = normaliseStringList(finding.references);
53
53
 
54
54
  const sections = [
55
55
  status(finding.result ?? resultTypes.UNKNOWN, summary, options),
56
56
  renderLocation(finding.location, options),
57
- renderSection("Evidence", evidence, options, false),
57
+ renderSection("Evidence", evidence, options, true),
58
58
  renderTextSection("Recommendation", finding.recommendation, options),
59
- renderSection("References", references, options, false),
59
+ renderSection("References", references, options, true),
60
60
  ].filter((section) => section !== "");
61
61
 
62
62
  return [renderTitle(title, options), ...sections].join("\n\n");
@@ -48,13 +48,13 @@ export function commandResult(result, options = {}) {
48
48
 
49
49
  const title = isNonEmptyString(result.title) ? result.title : defaultTitle;
50
50
  const metadata = renderMetadata(result, options);
51
- const details = normaliseStringList(result.details).map((detail) => `- ${detail}`);
51
+ const details = normaliseStringList(result.details);
52
52
  const summary = isNonEmptyString(result.summary) ? result.summary : "";
53
53
 
54
54
  const sections = [
55
55
  status(result.result ?? resultTypes.UNKNOWN, summary, options),
56
56
  metadata,
57
- renderSection("Details", details, options, false),
57
+ renderSection("Details", details, options, true),
58
58
  ].filter((section) => section !== "");
59
59
 
60
60
  return [renderTitle(title, options), ...sections].join("\n\n");
@@ -69,7 +69,8 @@ function renderChecks(checks, options) {
69
69
  status(check.result, formatLabel(check.name, check.detail, options), options),
70
70
  );
71
71
 
72
- return renderSection("Checks", lines, options);
72
+ // Status lines already start with a symbol, so only agent Markdown adds bullets.
73
+ return renderSection("Checks", lines, options, options.profile === profiles.AGENT);
73
74
  }
74
75
 
75
76
  /**
@@ -89,7 +90,8 @@ function renderFindings(findings, options) {
89
90
 
90
91
  const lines = validFindings.map((finding) => status(finding.result, finding.message, options));
91
92
 
92
- return renderSection("Findings", lines, options);
93
+ // Only agent Markdown adds bullets beside the status symbol.
94
+ return renderSection("Findings", lines, options, options.profile === profiles.AGENT);
93
95
  }
94
96
 
95
97
  /**
@@ -111,7 +113,8 @@ function renderSkippedChecks(skippedChecks, options) {
111
113
  status(resultTypes.SKIPPED, formatLabel(check.name, check.reason, options), options),
112
114
  );
113
115
 
114
- return renderSection("Skipped checks", lines, options);
116
+ // Only agent Markdown adds bullets beside the status symbol.
117
+ return renderSection("Skipped checks", lines, options, options.profile === profiles.AGENT);
115
118
  }
116
119
 
117
120
  /**
@@ -1,3 +1,4 @@
1
+ import { bulletList } from "../primitives/bullet-list.js";
1
2
  import { profiles } from "../profiles/profiles.js";
2
3
 
3
4
  /**
@@ -72,7 +73,8 @@ export function normaliseStringList(values) {
72
73
  * @param {object} options
73
74
  * Rendering options.
74
75
  * @param {boolean} listItems
75
- * Whether agent output should use bullet items.
76
+ * Whether to show each line as a bullet point, wrapped to the terminal
77
+ * width. Pass false for single values, commands, or numbered lines.
76
78
  * @returns {string}
77
79
  * Rendered section.
78
80
  */
@@ -83,10 +85,15 @@ export function renderSection(title, lines, options, listItems = true) {
83
85
 
84
86
  const heading = options.profile === profiles.AGENT ? `## ${title}` : title;
85
87
 
86
- const content =
87
- options.profile === profiles.AGENT && listItems ? lines.map((line) => `- ${line}`) : lines;
88
+ const content = listItems
89
+ ? bulletList(lines, {
90
+ profile: options.profile,
91
+ unicode: options.unicode,
92
+ width: options.width,
93
+ })
94
+ : lines.join("\n");
88
95
 
89
- return [heading, ...content].join("\n");
96
+ return [heading, content].join("\n");
90
97
  }
91
98
 
92
99
  /**
@@ -49,15 +49,13 @@ export function nextStepBlock(nextStep, options = {}) {
49
49
 
50
50
  const commands = renderCommands(nextStep.commands, options);
51
51
 
52
- const alternatives = normaliseStringList(nextStep.alternatives).map(
53
- (alternative) => `- ${alternative}`,
54
- );
52
+ const alternatives = normaliseStringList(nextStep.alternatives);
55
53
 
56
54
  const sections = [
57
55
  next,
58
56
  reason,
59
57
  commands,
60
- renderSection("Alternatives", alternatives, options, false),
58
+ renderSection("Alternatives", alternatives, options, true),
61
59
  ].filter((section) => section !== "");
62
60
 
63
61
  return [renderTitle(title, options), ...sections].join("\n\n");
@@ -47,13 +47,13 @@ export function taskSummary(summary, options = {}) {
47
47
  const title = isNonEmptyString(summary.title) ? summary.title : defaultTitle;
48
48
  const task = isNonEmptyString(summary.task) ? summary.task : "";
49
49
  const detail = formatLabel(task, summary.summary, options);
50
- const completed = normaliseStringList(summary.completed).map((item) => `- ${item}`);
51
- const remaining = normaliseStringList(summary.remaining).map((item) => `- ${item}`);
50
+ const completed = normaliseStringList(summary.completed);
51
+ const remaining = normaliseStringList(summary.remaining);
52
52
 
53
53
  const sections = [
54
54
  status(summary.result ?? resultTypes.UNKNOWN, detail, options),
55
- renderSection("Completed", completed, options, false),
56
- renderSection("Remaining", remaining, options, false),
55
+ renderSection("Completed", completed, options, true),
56
+ renderSection("Remaining", remaining, options, true),
57
57
  ].filter((section) => section !== "");
58
58
 
59
59
  return [renderTitle(title, options), ...sections].join("\n\n");
@@ -0,0 +1,150 @@
1
+ import { wrapText } from "../formatters/wrap.js";
2
+ import { getBulletSymbol } from "../theme/bullets.js";
3
+
4
+ /**
5
+ * Render items as a bullet list, wrapping each item's text beside its bullet.
6
+ *
7
+ * @param {object[]} items
8
+ * Item text, or `{ text, items }` for an item with nested items. Empty and
9
+ * malformed items are skipped.
10
+ * @param {object} options
11
+ * Rendering and wrapping options.
12
+ * @param {number} options.width
13
+ * Total line width used to calculate the available item width.
14
+ * @param {number} options.wrapWidth
15
+ * Explicit text width for each item.
16
+ * @returns {string}
17
+ * Rendered bullet list, or an empty string when no usable items exist.
18
+ */
19
+ export function bulletList(items = [], options = {}) {
20
+ const normalisedItems = normaliseItems(items);
21
+
22
+ if (normalisedItems.length === 0) {
23
+ return "";
24
+ }
25
+
26
+ return renderItems(normalisedItems, options, 0).join("\n");
27
+ }
28
+
29
+ /**
30
+ * Render one level of items and its nested children.
31
+ *
32
+ * @param {object[]} items
33
+ * Normalised entries at the current nesting level.
34
+ * @param {object} options
35
+ * Rendering and wrapping options.
36
+ * @param {number} depth
37
+ * Zero-based nesting depth.
38
+ * @returns {string[]}
39
+ * Rendered lines for this level and its children.
40
+ */
41
+ function renderItems(items, options, depth) {
42
+ const lines = [];
43
+
44
+ for (const entry of items) {
45
+ const prefix = createPrefix(depth, options);
46
+ const itemLines = wrapText(entry.text, resolveWrapWidth(prefix, options));
47
+
48
+ lines.push(
49
+ ...itemLines.map((line, index) => {
50
+ if (index === 0) {
51
+ return `${prefix}${line}`;
52
+ }
53
+
54
+ return `${" ".repeat(prefix.length)}${line}`;
55
+ }),
56
+ );
57
+
58
+ const childItems = normaliseItems(entry.items);
59
+
60
+ if (childItems.length > 0) {
61
+ lines.push(...renderItems(childItems, options, depth + 1));
62
+ }
63
+ }
64
+
65
+ return lines;
66
+ }
67
+
68
+ /**
69
+ * Build the plain-text prefix used for one item and its continuations.
70
+ *
71
+ * @param {number} depth
72
+ * Zero-based nesting depth.
73
+ * @param {object} options
74
+ * Symbol rendering options.
75
+ * @returns {string}
76
+ * Uncoloured indentation, bullet, and trailing space.
77
+ */
78
+ function createPrefix(depth, options) {
79
+ const symbol = getBulletSymbol(depth, options);
80
+ const gutter = `${symbol} `;
81
+ const indentation = " ".repeat(depth * gutter.length);
82
+
83
+ return `${indentation}${gutter}`;
84
+ }
85
+
86
+ /**
87
+ * Resolve the text width available after the item prefix.
88
+ *
89
+ * @param {string} prefix
90
+ * Plain-text item prefix.
91
+ * @param {object} options
92
+ * Rendering and wrapping options.
93
+ * @returns {number|undefined}
94
+ * Explicit text width, derived text width, or undefined for wrapText's default.
95
+ */
96
+ function resolveWrapWidth(prefix, options) {
97
+ if (options.wrapWidth !== undefined) {
98
+ return options.wrapWidth;
99
+ }
100
+
101
+ if (Number.isFinite(options.width)) {
102
+ return Math.max(1, options.width - prefix.length);
103
+ }
104
+
105
+ return undefined;
106
+ }
107
+
108
+ /**
109
+ * Convert a list value into renderable entries, dropping unusable items.
110
+ *
111
+ * @param {*} items
112
+ * Requested list entries.
113
+ * @returns {object[]}
114
+ * Non-empty strings and valid nested entries.
115
+ */
116
+ function normaliseItems(items) {
117
+ if (!Array.isArray(items)) {
118
+ return [];
119
+ }
120
+
121
+ return items.map(normaliseItem).filter((entry) => entry !== null);
122
+ }
123
+
124
+ /**
125
+ * Convert one supported input value into a list entry.
126
+ *
127
+ * @param {*} item
128
+ * Requested list value.
129
+ * @returns {object|null}
130
+ * Original object or a new entry for a string, or null when the value has no usable text.
131
+ */
132
+ function normaliseItem(item) {
133
+ if (typeof item === "string" && item.trim() !== "") {
134
+ return {
135
+ items: [],
136
+ text: item,
137
+ };
138
+ }
139
+
140
+ if (
141
+ item !== null &&
142
+ typeof item === "object" &&
143
+ typeof item.text === "string" &&
144
+ item.text.trim() !== ""
145
+ ) {
146
+ return item;
147
+ }
148
+
149
+ return null;
150
+ }
@@ -0,0 +1,41 @@
1
+ import { profiles } from "../profiles/profiles.js";
2
+
3
+ // Bullet symbols for Unicode, ASCII, and Markdown-compatible agent output.
4
+ export const bulletTokens = {
5
+ agent: "-",
6
+ ascii: "*",
7
+ unicode: {
8
+ nested: "◦",
9
+ root: "•",
10
+ },
11
+ };
12
+
13
+ /**
14
+ * Resolve the display symbol for one bullet depth.
15
+ *
16
+ * @param {number} depth
17
+ * Zero-based nesting depth.
18
+ * @param {object} options
19
+ * Symbol rendering options.
20
+ * @param {string} options.profile
21
+ * Active output profile.
22
+ * @param {boolean} options.unicode
23
+ * Whether to return the Unicode symbol.
24
+ * @returns {string}
25
+ * Display symbol for the requested bullet depth and profile.
26
+ */
27
+ export function getBulletSymbol(depth = 0, options = {}) {
28
+ if (options.profile === profiles.AGENT) {
29
+ return bulletTokens.agent;
30
+ }
31
+
32
+ if (
33
+ options.profile === profiles.CI ||
34
+ options.profile === profiles.PLAIN ||
35
+ options.unicode === false
36
+ ) {
37
+ return bulletTokens.ascii;
38
+ }
39
+
40
+ return depth === 0 ? bulletTokens.unicode.root : bulletTokens.unicode.nested;
41
+ }