@lewishowles/cli-style 0.12.1 → 0.14.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";
@@ -764,12 +765,21 @@ function renderPrimitives(options) {
764
765
  "Tables",
765
766
  renderTableExample(options),
766
767
  "",
768
+ "Truncated tables",
769
+ renderOverflowTableExample(options, "truncate"),
770
+ "",
771
+ "Wrapped tables",
772
+ renderOverflowTableExample(options, "wrap"),
773
+ "",
767
774
  "Narrow tables",
768
775
  renderTableExample({
769
776
  ...options,
770
777
  width: 18,
771
778
  }),
772
779
  "",
780
+ "Bullet lists",
781
+ renderBulletListExamples(options),
782
+ "",
773
783
  "Feedback",
774
784
  hint(`Run ${span("npm run docs:readme", "info", options)}`, options),
775
785
  labelledLine("Notice", "Reusable labelled output", {
@@ -866,6 +876,42 @@ function renderPrimitives(options) {
866
876
  ].join("\n");
867
877
  }
868
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
+
869
915
  /**
870
916
  * Render wide or narrow table diagnostics.
871
917
  *
@@ -900,6 +946,40 @@ function renderTableExample(options) {
900
946
  });
901
947
  }
902
948
 
949
+ /**
950
+ * Render a table with a long-detail column capped by maxWidth.
951
+ *
952
+ * @param {object} options
953
+ * Rendering options.
954
+ * @param {string} overflow
955
+ * Overflow mode ('truncate' or 'wrap') for the capped column.
956
+ * @returns {string}
957
+ * Table example.
958
+ */
959
+ function renderOverflowTableExample(options, overflow) {
960
+ return table({
961
+ ...options,
962
+ columns: [
963
+ {
964
+ key: "source",
965
+ label: "Source",
966
+ },
967
+ {
968
+ key: "detail",
969
+ label: "Detail",
970
+ maxWidth: 28,
971
+ overflow,
972
+ },
973
+ ],
974
+ rows: [
975
+ {
976
+ detail: "packages/cli-style/src/primitives/table.js",
977
+ source: "Table",
978
+ },
979
+ ],
980
+ });
981
+ }
982
+
903
983
  /**
904
984
  * Render a chart that exposes positive, warning, and negative colours.
905
985
  *
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
+ }
@@ -1,9 +1,7 @@
1
1
  import { foreground } from "../formatters/ansi.js";
2
+ import { defaultWidth, minimumWidth, normaliseWidth } from "../formatters/width.js";
2
3
  import { tableColours } from "../theme/colours.js";
3
4
 
4
- // Tables assume a standard terminal width when no capability is provided.
5
- const defaultWidth = 80;
6
-
7
5
  // Columns use a compact but readable gap.
8
6
  const columnSeparator = " ";
9
7
 
@@ -15,7 +13,9 @@ const columnSeparator = " ";
15
13
  * @param {boolean} options.colour
16
14
  * Whether ANSI colour should be applied.
17
15
  * @param {object[]} options.columns
18
- * Column keys and labels.
16
+ * Column keys and labels. A column may set maxWidth to cap its width,
17
+ * with overflow ('truncate', the default, or 'wrap') controlling how
18
+ * values beyond that width render.
19
19
  * @param {object[]} options.rows
20
20
  * Table records.
21
21
  * @param {boolean} options.unicode
@@ -39,7 +39,18 @@ export function table(options = {}) {
39
39
  widths.reduce((total, width) => total + width, 0) +
40
40
  columnSeparator.length * (columns.length - 1);
41
41
 
42
- const availableWidth = Number.isFinite(options.width) ? options.width : defaultWidth;
42
+ const requestedWidth = Number.isFinite(options.width) ? options.width : undefined;
43
+
44
+ // Let a requested width smaller than minimumWidth pass through unclamped.
45
+ const minimumTableWidth =
46
+ requestedWidth === undefined
47
+ ? minimumWidth
48
+ : Math.max(1, Math.min(requestedWidth, minimumWidth));
49
+
50
+ const availableWidth = normaliseWidth(requestedWidth, {
51
+ defaultWidth,
52
+ minWidth: minimumTableWidth,
53
+ });
43
54
 
44
55
  if (naturalWidth > availableWidth) {
45
56
  return renderBlocks(columns, rows, options);
@@ -99,9 +110,65 @@ function normaliseRows(rows) {
99
110
  * Column widths.
100
111
  */
101
112
  function columnWidths(columns, rows) {
102
- return columns.map((column) =>
103
- Math.max(column.label.length, ...rows.map((row) => cellValue(row[column.key]).length)),
104
- );
113
+ return columns.map((column) => {
114
+ const naturalWidth = Math.max(
115
+ column.label.length,
116
+ ...rows.map((row) => cellValue(row[column.key]).length),
117
+ );
118
+
119
+ const maxWidth = columnMaxWidth(column);
120
+
121
+ return maxWidth === null ? naturalWidth : Math.min(naturalWidth, maxWidth);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Return a column's configured width cap, if any.
127
+ *
128
+ * @param {object} column
129
+ * Table column.
130
+ * @returns {number|null}
131
+ * Normalised maxWidth, or null when the column has none.
132
+ */
133
+ function columnMaxWidth(column) {
134
+ if (!Number.isFinite(column.maxWidth) || column.maxWidth < 1) {
135
+ return null;
136
+ }
137
+
138
+ return normaliseWidth(column.maxWidth, {
139
+ defaultWidth: 1,
140
+ minWidth: 1,
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Split a cell's value into the lines it renders as within a fixed-width column.
146
+ *
147
+ * @param {string} value
148
+ * Cell value.
149
+ * @param {object} column
150
+ * Table column, read for its overflow mode.
151
+ * @param {number} width
152
+ * Column width.
153
+ * @returns {string[]}
154
+ * Lines the cell renders as; truncated to one line with a trailing
155
+ * ellipsis unless the column's overflow is 'wrap'.
156
+ */
157
+ function columnLines(value, column, width) {
158
+ if (value.length <= width) {
159
+ return [value];
160
+ }
161
+
162
+ if (column.overflow === "wrap") {
163
+ return Array.from(
164
+ {
165
+ length: Math.ceil(value.length / width),
166
+ },
167
+ (_, index) => value.slice(index * width, (index + 1) * width),
168
+ );
169
+ }
170
+
171
+ return [`${value.slice(0, width - 1)}…`];
105
172
  }
106
173
 
107
174
  /**
@@ -120,9 +187,11 @@ function columnWidths(columns, rows) {
120
187
  */
121
188
  function renderTable(columns, rows, widths, options) {
122
189
  const header = columns
123
- .map((column, index) =>
124
- renderHeader(padColumn(column.label, widths[index], index, columns.length), options),
125
- )
190
+ .map((column, index) => {
191
+ const headerLine = columnLines(column.label, { overflow: "truncate" }, widths[index])[0];
192
+
193
+ return renderHeader(padColumn(headerLine, widths[index], index, columns.length), options);
194
+ })
126
195
  .join(columnSeparator);
127
196
 
128
197
  const ruleCharacter = options.unicode === false ? "-" : "─";
@@ -131,17 +200,29 @@ function renderTable(columns, rows, widths, options) {
131
200
  const renderedRule =
132
201
  options.colour === true ? foreground(rule, tableColours.border, options) : rule;
133
202
 
134
- const body = rows.map((row) =>
135
- columns
136
- .map((column, index) =>
137
- renderCell(
138
- padColumn(cellValue(row[column.key]), widths[index], index, columns.length),
139
- index,
140
- options,
141
- ),
142
- )
143
- .join(columnSeparator),
144
- );
203
+ const body = rows.flatMap((row) => {
204
+ const lines = columns.map((column, index) =>
205
+ columnLines(cellValue(row[column.key]), column, widths[index]),
206
+ );
207
+
208
+ const lineCount = Math.max(...lines.map((column) => column.length));
209
+
210
+ return Array.from(
211
+ {
212
+ length: lineCount,
213
+ },
214
+ (_, lineIndex) =>
215
+ columns
216
+ .map((column, index) =>
217
+ renderCell(
218
+ padColumn(lines[index][lineIndex] ?? "", widths[index], index, columns.length),
219
+ index,
220
+ options,
221
+ ),
222
+ )
223
+ .join(columnSeparator),
224
+ );
225
+ });
145
226
 
146
227
  return [header, renderedRule, ...body].join("\n");
147
228
  }
@@ -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
+ }