@oclif/multi-stage-output 0.4.2 → 0.4.3-dev.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.
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- export declare function Divider({ dividerChar, dividerColor, padding, textColor, textPadding: titlePadding, title, width, }: {
2
+ export declare function Divider({ dividerChar, dividerColor, padding, terminalWidth, textColor, textPadding, title, width, }: {
3
3
  readonly title?: string;
4
4
  readonly width?: number | 'full';
5
5
  readonly padding?: number;
@@ -7,4 +7,5 @@ export declare function Divider({ dividerChar, dividerColor, padding, textColor,
7
7
  readonly textPadding?: number;
8
8
  readonly dividerChar?: string;
9
9
  readonly dividerColor?: string;
10
+ readonly terminalWidth?: number;
10
11
  }): React.ReactNode;
@@ -3,11 +3,17 @@ import React from 'react';
3
3
  const getSideDividerWidth = (width, titleWidth) => (width - titleWidth) / 2;
4
4
  const getNumberOfCharsPerWidth = (char, width) => width / char.length;
5
5
  const PAD = ' ';
6
- export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, textColor, textPadding: titlePadding = 1, title = '', width = 50, }) {
7
- const titleString = title ? `${PAD.repeat(titlePadding) + title + PAD.repeat(titlePadding)}` : '';
6
+ export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, terminalWidth = process.stdout.columns ?? 80, textColor, textPadding = 1, title = '', width = 50, }) {
7
+ const titleString = title ? `${PAD.repeat(textPadding) + title + PAD.repeat(textPadding)}` : '';
8
8
  const titleWidth = titleString.length;
9
- const terminalWidth = process.stdout.columns ?? 80;
10
- const widthToUse = width === 'full' ? terminalWidth - titlePadding : width > terminalWidth ? terminalWidth : width;
9
+ const widthToUse = width === 'full'
10
+ ? // if the width is `full`, use the terminal width minus the padding and title padding
11
+ terminalWidth - textPadding - padding
12
+ : // otherwise, if the provided width is greater than the terminal width, use the terminal width minus the padding and title paddding
13
+ width > terminalWidth
14
+ ? terminalWidth - textPadding - padding
15
+ : // otherwise, use the provided width
16
+ width;
11
17
  const dividerWidth = getSideDividerWidth(widthToUse, titleWidth);
12
18
  const numberOfCharsPerSide = getNumberOfCharsPerWidth(dividerChar, dividerWidth);
13
19
  const dividerSideString = dividerChar.repeat(numberOfCharsPerSide);
@@ -1,6 +1,6 @@
1
1
  import { type SpinnerName } from 'cli-spinners';
2
2
  import React from 'react';
3
- import { IconProps } from './icon.js';
3
+ import { RequiredDesign } from '../design.js';
4
4
  type UseSpinnerProps = {
5
5
  /**
6
6
  * Type of a spinner.
@@ -19,13 +19,13 @@ type SpinnerProps = UseSpinnerProps & {
19
19
  readonly labelPosition?: 'left' | 'right';
20
20
  };
21
21
  export declare function Spinner({ isBold, label, labelPosition, type }: SpinnerProps): React.ReactElement;
22
- export declare function SpinnerOrError({ error, failedIcon, labelPosition, ...props }: SpinnerProps & {
22
+ export declare function SpinnerOrError({ design, error, labelPosition, ...props }: SpinnerProps & {
23
23
  readonly error?: Error;
24
- readonly failedIcon: IconProps;
24
+ readonly design: RequiredDesign;
25
25
  }): React.ReactElement;
26
26
  export declare function SpinnerOrErrorOrChildren({ children, error, ...props }: SpinnerProps & {
27
27
  readonly children?: React.ReactNode;
28
28
  readonly error?: Error;
29
- readonly failedIcon: IconProps;
29
+ readonly design: RequiredDesign;
30
30
  }): React.ReactElement;
31
31
  export {};
@@ -31,13 +31,13 @@ export function Spinner({ isBold, label, labelPosition = 'right', type }) {
31
31
  " ",
32
32
  label)));
33
33
  }
34
- export function SpinnerOrError({ error, failedIcon, labelPosition = 'right', ...props }) {
34
+ export function SpinnerOrError({ design, error, labelPosition = 'right', ...props }) {
35
35
  if (error) {
36
36
  return (React.createElement(Box, null,
37
37
  props.label && labelPosition === 'left' && React.createElement(Text, null,
38
38
  props.label,
39
39
  " "),
40
- React.createElement(Icon, { icon: failedIcon }),
40
+ React.createElement(Icon, { icon: design.icons.failed }),
41
41
  props.label && labelPosition === 'right' && React.createElement(Text, null,
42
42
  " ",
43
43
  props.label)));
@@ -24,6 +24,10 @@ type Info<T extends Record<string, unknown>> = {
24
24
  * Whether the value should be bold.
25
25
  */
26
26
  bold?: boolean;
27
+ /**
28
+ * Set to `true` to prevent this key-value pair or message from being collapsed when the window is too short. Defaults to false.
29
+ */
30
+ neverCollapse?: boolean;
27
31
  };
28
32
  export type KeyValuePair<T extends Record<string, unknown>> = Info<T> & {
29
33
  /**
@@ -48,18 +52,39 @@ export type FormattedKeyValue = {
48
52
  readonly value: string | undefined;
49
53
  readonly stage?: string;
50
54
  readonly type: 'dynamic-key-value' | 'static-key-value' | 'message';
55
+ readonly neverCollapse?: boolean;
51
56
  };
52
57
  export type StagesProps = {
58
+ readonly compactionLevel?: number;
53
59
  readonly design?: RequiredDesign;
54
60
  readonly error?: Error | undefined;
61
+ readonly hasElapsedTime?: boolean;
62
+ readonly hasStageTime?: boolean;
55
63
  readonly postStagesBlock?: FormattedKeyValue[];
56
64
  readonly preStagesBlock?: FormattedKeyValue[];
57
65
  readonly stageSpecificBlock?: FormattedKeyValue[];
58
- readonly title?: string;
59
- readonly hasElapsedTime?: boolean;
60
- readonly hasStageTime?: boolean;
61
- readonly timerUnit?: 'ms' | 's';
62
66
  readonly stageTracker: StageTracker;
67
+ readonly timerUnit?: 'ms' | 's';
68
+ readonly title?: string;
69
+ };
70
+ /**
71
+ * Determine the level of compaction required to render the stages component within the terminal height.
72
+ *
73
+ * Compaction levels:
74
+ * 0 - hide nothing
75
+ * 1 - only show one stage at a time, with stage specific info nested under the stage
76
+ * 2 - hide the elapsed time
77
+ * 3 - hide the title
78
+ * 4 - hide the pre-stages block
79
+ * 5 - hide the post-stages block
80
+ * 6 - put the stage specific info directly next to the stage
81
+ * 7 - hide the stage-specific block
82
+ * 8 - reduce the padding between boxes
83
+ * @returns the compaction level based on the number of lines that will be displayed
84
+ */
85
+ export declare function determineCompactionLevel({ design, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, title, }: StagesProps, rows: number, columns: number): {
86
+ compactionLevel: number;
87
+ totalHeight: number;
63
88
  };
64
- export declare function Stages({ design, error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode;
89
+ export declare function Stages({ compactionLevel, design, error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode;
65
90
  export {};
@@ -1,5 +1,5 @@
1
1
  import { capitalCase } from 'change-case';
2
- import { Box, Text } from 'ink';
2
+ import { Box, Text, useStdout } from 'ink';
3
3
  import React from 'react';
4
4
  import { constructDesignParams } from '../design.js';
5
5
  import { Divider } from './divider.js';
@@ -33,7 +33,7 @@ function StageInfos({ design, error, keyValuePairs, stage, }) {
33
33
  if (kv.type === 'dynamic-key-value') {
34
34
  return (React.createElement(Box, { key: key },
35
35
  React.createElement(Icon, { icon: design.icons.info }),
36
- React.createElement(SpinnerOrErrorOrChildren, { error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value)))));
36
+ React.createElement(SpinnerOrErrorOrChildren, { error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, design: design }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value)))));
37
37
  }
38
38
  if (kv.type === 'static-key-value') {
39
39
  return (React.createElement(Box, { key: key },
@@ -50,7 +50,7 @@ function Infos({ design, error, keyValuePairs, }) {
50
50
  return React.createElement(SimpleMessage, { key: key, ...kv });
51
51
  }
52
52
  if (kv.type === 'dynamic-key-value') {
53
- return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))));
53
+ return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, design: design }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))));
54
54
  }
55
55
  if (kv.type === 'static-key-value') {
56
56
  return React.createElement(StaticKeyValue, { key: key, ...kv });
@@ -58,29 +58,248 @@ function Infos({ design, error, keyValuePairs, }) {
58
58
  return false;
59
59
  });
60
60
  }
61
- export function Stages({ design = constructDesignParams(), error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) {
62
- return (React.createElement(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1 },
63
- title && React.createElement(Divider, { title: title, ...design.title }),
64
- preStagesBlock && preStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
65
- React.createElement(Infos, { design: design, error: error, keyValuePairs: preStagesBlock }))),
66
- React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" },
67
- React.createElement(Box, null,
68
- (status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: design.spinners.stage, failedIcon: design.icons.failed })),
69
- status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped },
70
- React.createElement(Text, { color: "dim" },
71
- capitalCase(stage),
72
- " - Skipped"))),
73
- status === 'completed' && (React.createElement(Icon, { icon: design.icons.completed },
74
- React.createElement(Text, null, capitalCase(stage)))),
75
- status === 'pending' && (React.createElement(Icon, { icon: design.icons.pending },
76
- React.createElement(Text, null, capitalCase(stage)))),
77
- status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, null,
78
- React.createElement(Text, null, " "),
79
- React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))),
80
- stageSpecificBlock && stageSpecificBlock.length > 0 && status !== 'pending' && status !== 'skipped' && (React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))))),
81
- postStagesBlock && postStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 },
82
- React.createElement(Infos, { design: design, error: error, keyValuePairs: postStagesBlock }))),
83
- hasElapsedTime && (React.createElement(Box, { marginLeft: 1 },
61
+ function CompactStage({ design, direction = 'row', error, stage, stageSpecificBlock, stageTracker, status, }) {
62
+ if (status !== 'current')
63
+ return false;
64
+ return (React.createElement(Box, { flexDirection: direction },
65
+ React.createElement(SpinnerOrError, { error: error, label: `[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${capitalCase(stage)}`, type: design.spinners.stage, design: design }),
66
+ stageSpecificBlock && stageSpecificBlock.length > 0 && (React.createElement(Box, { flexDirection: "column" },
67
+ React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))));
68
+ }
69
+ function Stage({ design, error, stage, status, }) {
70
+ return (React.createElement(Box, null,
71
+ (status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: design.spinners.stage, design: design })),
72
+ status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped },
73
+ React.createElement(Text, { color: "dim" },
74
+ capitalCase(stage),
75
+ " - Skipped"))),
76
+ status !== 'skipped' && status !== 'failed' && status !== 'current' && (React.createElement(Icon, { icon: design.icons[status] },
77
+ React.createElement(Text, null, capitalCase(stage))))));
78
+ }
79
+ function StageEntries({ compactionLevel, design, error, hasStageTime, stageSpecificBlock, stageTracker, timerUnit, }) {
80
+ return (React.createElement(React.Fragment, null, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" },
81
+ React.createElement(Box, null,
82
+ compactionLevel === 0 ? (React.createElement(Stage, { stage: stage, status: status, design: design, error: error })) : (
83
+ // Render the stage name, spinner, and stage specific info
84
+ React.createElement(CompactStage, { stage: stage, status: status, design: design, error: error, stageSpecificBlock: stageSpecificBlock, stageTracker: stageTracker, direction: compactionLevel >= 6 ? 'row' : 'column' })),
85
+ status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, { display: compactionLevel === 0 ? 'flex' : status === 'current' ? 'flex' : 'none' },
86
+ React.createElement(Text, null, " "),
87
+ React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))),
88
+ compactionLevel === 0 &&
89
+ stageSpecificBlock &&
90
+ stageSpecificBlock.length > 0 &&
91
+ status !== 'pending' &&
92
+ status !== 'skipped' && (React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))))));
93
+ }
94
+ function filterInfos(infos, compactionLevel, cutOff) {
95
+ return infos.filter((info) => {
96
+ // return true to keep the info
97
+ if (compactionLevel < cutOff || info.neverCollapse) {
98
+ return true;
99
+ }
100
+ return false;
101
+ });
102
+ }
103
+ /**
104
+ * Determine the level of compaction required to render the stages component within the terminal height.
105
+ *
106
+ * Compaction levels:
107
+ * 0 - hide nothing
108
+ * 1 - only show one stage at a time, with stage specific info nested under the stage
109
+ * 2 - hide the elapsed time
110
+ * 3 - hide the title
111
+ * 4 - hide the pre-stages block
112
+ * 5 - hide the post-stages block
113
+ * 6 - put the stage specific info directly next to the stage
114
+ * 7 - hide the stage-specific block
115
+ * 8 - reduce the padding between boxes
116
+ * @returns the compaction level based on the number of lines that will be displayed
117
+ */
118
+ export function determineCompactionLevel({ design = constructDesignParams(), hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, title, }, rows, columns) {
119
+ const calculateHeightOfBlock = (block) => {
120
+ if (!block)
121
+ return 0;
122
+ return block.reduce((acc, info) => {
123
+ if (info.type === 'message') {
124
+ if (!info.value)
125
+ return acc;
126
+ if (info.value.length > columns) {
127
+ // if the message is longer than the terminal width, add the number of lines
128
+ return acc + Math.ceil(info.value.length / columns);
129
+ }
130
+ // if the message is multiline, add the number of lines
131
+ return acc + info.value.split('\n').length;
132
+ }
133
+ const { label = '', value } = info;
134
+ // if there's no value we still add 1 for the label
135
+ if (!value)
136
+ return acc + 1;
137
+ if (label.length + Number(': '.length) + value.length > columns) {
138
+ // if the value is longer than the terminal width, add the number of lines
139
+ return acc + Math.ceil(value.length / columns);
140
+ }
141
+ return acc + value.split('\n').length;
142
+ }, 0);
143
+ };
144
+ const calculateHeightOfStage = (stage) => {
145
+ const status = stageTracker.get(stage) ?? 'pending';
146
+ const skipped = status === 'skipped' ? ' - Skipped' : '';
147
+ // We don't have access to the exact stage time, so we're taking a conservative estimate of
148
+ // 10 characters + 1 character for the space between the stage and timer,
149
+ // examples: 999ms (5), 59.99s (6), 59m 59.99s (10), 23h 59m (7)
150
+ const stageTimeLength = hasStageTime ? 11 : 0;
151
+ if (
152
+ // 1 for the left margin
153
+ 1 +
154
+ design.icons[status].paddingLeft +
155
+ design.icons[status].figure.length +
156
+ design.icons[status].paddingRight +
157
+ stage.length +
158
+ skipped.length +
159
+ stageTimeLength >
160
+ columns) {
161
+ return Math.ceil(stage.length / columns);
162
+ }
163
+ return 1;
164
+ };
165
+ const calculateWidthOfCompactStage = (stage) => {
166
+ const status = stageTracker.get(stage) ?? 'current';
167
+ // We don't have access to the exact stage time, so we're taking a conservative estimate of
168
+ // 7 characters + 1 character for the space between the stage and timer,
169
+ // examples: 999ms (5), 59s (3), 59m 59s (7), 23h 59m (7)
170
+ const stageTimeLength = hasStageTime ? 8 : 0;
171
+ const firstStageSpecificBlock = stageSpecificBlock?.find((block) => block.stage === stage);
172
+ const firstStageSpecificBlockLength = firstStageSpecificBlock?.type === 'message'
173
+ ? (firstStageSpecificBlock?.value?.length ?? 0)
174
+ : (firstStageSpecificBlock?.label?.length ?? 0) + (firstStageSpecificBlock?.value?.length ?? 0) + 2;
175
+ const width =
176
+ // 1 for the left margin
177
+ 1 +
178
+ design.icons[status].paddingLeft +
179
+ design.icons[status].figure.length +
180
+ design.icons[status].paddingRight +
181
+ `[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${stage}`.length +
182
+ stageTimeLength +
183
+ firstStageSpecificBlockLength;
184
+ return width;
185
+ };
186
+ const stagesHeight = [...stageTracker.values()].reduce((acc, stage) => acc + calculateHeightOfStage(stage), 0);
187
+ const preStagesBlockHeight = calculateHeightOfBlock(preStagesBlock);
188
+ const postStagesBlockHeight = calculateHeightOfBlock(postStagesBlock);
189
+ const stageSpecificBlockHeight = calculateHeightOfBlock(stageSpecificBlock);
190
+ // 3 at minimum because: 1 for marginTop on entire component, 1 for marginBottom on entire component, 1 for paddingBottom on StageEntries
191
+ const paddings = 3 + (preStagesBlock ? 1 : 0) + (postStagesBlock ? 1 : 0) + (title ? 1 : 0);
192
+ const totalHeight = stagesHeight +
193
+ preStagesBlockHeight +
194
+ postStagesBlockHeight +
195
+ stageSpecificBlockHeight +
196
+ (title ? 1 : 0) +
197
+ (hasElapsedTime ? 1 : 0) +
198
+ paddings +
199
+ // add one for good measure - iTerm2 will flicker on every render if the height is exactly the same as the terminal height so it's better to be safe
200
+ 1;
201
+ let cLevel = 0;
202
+ const levels = [
203
+ // 1: only show one stage at a time, with stage specific info nested under the stage
204
+ (remainingHeight) => remainingHeight - stagesHeight + 1,
205
+ // 2: hide the elapsed time
206
+ (remainingHeight) => remainingHeight - 1,
207
+ // 3: hide the title (subtract 1 for title and 1 for paddingBottom)
208
+ (remainingHeight) => remainingHeight - 2,
209
+ // 4: hide the pre-stages block (subtract 1 for paddingBottom)
210
+ (remainingHeight) => remainingHeight - preStagesBlockHeight - 1,
211
+ // 5: hide the post-stages block
212
+ (remainingHeight) => remainingHeight - postStagesBlockHeight,
213
+ // 6: put the stage specific info directly next to the stage
214
+ (remainingHeight) => remainingHeight - stageSpecificBlockHeight,
215
+ // 7: hide the stage-specific block
216
+ (remainingHeight) => remainingHeight - stageSpecificBlockHeight,
217
+ // 8: reduce the padding between boxes
218
+ (remainingHeight) => remainingHeight - 1,
219
+ ];
220
+ let remainingHeight = totalHeight;
221
+ while (cLevel < 8 && remainingHeight >= rows) {
222
+ remainingHeight = levels[cLevel](remainingHeight);
223
+ cLevel++;
224
+ }
225
+ // It's possible that the collapsed stage might extend beyond the terminal width.
226
+ // If so, we need to bump the compaction level up to 7 so that the stage specific info is hidden
227
+ if (cLevel === 6 && stageTracker.current && calculateWidthOfCompactStage(stageTracker.current) >= columns) {
228
+ cLevel = 7;
229
+ }
230
+ return {
231
+ compactionLevel: cLevel,
232
+ totalHeight,
233
+ };
234
+ }
235
+ export function Stages({ compactionLevel, design = constructDesignParams(), error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) {
236
+ const { stdout } = useStdout();
237
+ const [levelOfCompaction, setLevelOfCompaction] = React.useState(determineCompactionLevel({
238
+ hasElapsedTime,
239
+ hasStageTime,
240
+ postStagesBlock,
241
+ preStagesBlock,
242
+ stageSpecificBlock,
243
+ stageTracker,
244
+ title,
245
+ }, stdout.rows - 1, stdout.columns).compactionLevel);
246
+ React.useEffect(() => {
247
+ setLevelOfCompaction(determineCompactionLevel({
248
+ hasElapsedTime,
249
+ hasStageTime,
250
+ postStagesBlock,
251
+ preStagesBlock,
252
+ stageSpecificBlock,
253
+ stageTracker,
254
+ title,
255
+ }, stdout.rows - 1, stdout.columns).compactionLevel);
256
+ }, [
257
+ compactionLevel,
258
+ hasElapsedTime,
259
+ hasStageTime,
260
+ postStagesBlock,
261
+ preStagesBlock,
262
+ stageSpecificBlock,
263
+ stageTracker,
264
+ stdout.columns,
265
+ stdout.rows,
266
+ title,
267
+ ]);
268
+ React.useEffect(() => {
269
+ const handler = () => {
270
+ setLevelOfCompaction(determineCompactionLevel({
271
+ hasElapsedTime,
272
+ hasStageTime,
273
+ postStagesBlock,
274
+ preStagesBlock,
275
+ stageSpecificBlock,
276
+ stageTracker,
277
+ title,
278
+ }, stdout.rows - 1, stdout.columns).compactionLevel);
279
+ };
280
+ stdout.on('resize', handler);
281
+ return () => {
282
+ stdout.removeListener('resize', handler);
283
+ };
284
+ });
285
+ // if compactionLevel is provided, use that instead of the calculated level
286
+ const actualLevelOfCompaction = compactionLevel ?? levelOfCompaction;
287
+ // filter out the info blocks based on the compaction level
288
+ const preStages = filterInfos(preStagesBlock ?? [], actualLevelOfCompaction, 4);
289
+ const postStages = filterInfos(postStagesBlock ?? [], actualLevelOfCompaction, 5);
290
+ const stageSpecific = filterInfos(stageSpecificBlock ?? [], actualLevelOfCompaction, 7);
291
+ // Reduce padding if the compaction level is 8
292
+ const padding = actualLevelOfCompaction === 8 ? 0 : 1;
293
+ return (React.createElement(Box, { flexDirection: "column", marginTop: padding, marginBottom: padding },
294
+ actualLevelOfCompaction < 3 && title && (React.createElement(Box, { paddingBottom: padding },
295
+ React.createElement(Divider, { title: title, ...design.title, terminalWidth: stdout.columns }))),
296
+ preStages && preStages.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingBottom: padding },
297
+ React.createElement(Infos, { design: design, error: error, keyValuePairs: preStages }))),
298
+ React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingBottom: padding },
299
+ React.createElement(StageEntries, { compactionLevel: actualLevelOfCompaction, design: design, error: error, hasStageTime: hasStageTime, stageSpecificBlock: stageSpecific, stageTracker: stageTracker, timerUnit: timerUnit })),
300
+ postStages && postStages.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1 },
301
+ React.createElement(Infos, { design: design, error: error, keyValuePairs: postStages }))),
302
+ hasElapsedTime && (React.createElement(Box, { marginLeft: 1, display: actualLevelOfCompaction < 2 ? 'flex' : 'none' },
84
303
  React.createElement(Text, null, "Elapsed Time: "),
85
304
  React.createElement(Timer, { unit: timerUnit })))));
86
305
  }
package/lib/design.d.ts CHANGED
@@ -3,7 +3,7 @@ import { IconProps } from './components/icon.js';
3
3
  export type Design = {
4
4
  icons?: {
5
5
  /**
6
- * Icon to display for a completed stage. Defaults to '✔'
6
+ * Icon to display for a completed stage. Defaults to green '✔'
7
7
  */
8
8
  completed?: IconProps;
9
9
  /**
@@ -13,21 +13,33 @@ export type Design = {
13
13
  */
14
14
  current?: IconProps;
15
15
  /**
16
- * Icon to display for a failed stage. Defaults to '✘'
16
+ * Icon to display for a failed stage. Defaults to red '✘'
17
17
  */
18
18
  failed?: IconProps;
19
19
  /**
20
- * Icon to display for a pending stage. Defaults to '◼'
20
+ * Icon to display for a pending stage. Defaults to dim '◼'
21
21
  */
22
22
  pending?: IconProps;
23
23
  /**
24
- * Icon to display for a skipped stage. Defaults to '◯'
24
+ * Icon to display for a skipped stage. Defaults to dim '◯'
25
25
  */
26
26
  skipped?: IconProps;
27
27
  /**
28
28
  * Icon to display for stage specific information. Defaults to '▸'
29
29
  */
30
30
  info?: IconProps;
31
+ /**
32
+ * Icon to display for a aborted stage. Defaults to red '◼'
33
+ */
34
+ aborted?: IconProps;
35
+ /**
36
+ * Icon to display for a paused stage. Defaults to magenta '●'
37
+ */
38
+ paused?: IconProps;
39
+ /**
40
+ * Icon to display for an async stage. Defaults to magenta '▶'
41
+ */
42
+ async?: IconProps;
31
43
  };
32
44
  title?: {
33
45
  /**
package/lib/design.js CHANGED
@@ -2,6 +2,20 @@ import figures from 'figures';
2
2
  export function constructDesignParams(design) {
3
3
  return {
4
4
  icons: {
5
+ aborted: {
6
+ color: 'red',
7
+ figure: figures.squareSmallFilled,
8
+ paddingLeft: 0,
9
+ paddingRight: 0,
10
+ ...design?.icons?.current,
11
+ },
12
+ async: {
13
+ color: 'magenta',
14
+ figure: figures.play,
15
+ paddingLeft: 0,
16
+ paddingRight: 0,
17
+ ...design?.icons?.current,
18
+ },
5
19
  completed: {
6
20
  color: 'green',
7
21
  figure: figures.tick,
@@ -30,6 +44,13 @@ export function constructDesignParams(design) {
30
44
  paddingRight: 1,
31
45
  ...design?.icons?.info,
32
46
  },
47
+ paused: {
48
+ color: 'magenta',
49
+ figure: figures.bullet,
50
+ paddingLeft: 0,
51
+ paddingRight: 1,
52
+ ...design?.icons?.current,
53
+ },
33
54
  pending: {
34
55
  color: 'dim',
35
56
  figure: figures.squareSmallFilled,
@@ -52,17 +52,17 @@ export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
52
52
  readonly jsonEnabled: boolean;
53
53
  };
54
54
  export declare class MultiStageOutput<T extends Record<string, unknown>> implements Disposable {
55
- private ciInstance;
55
+ private readonly ciInstance;
56
56
  private data?;
57
57
  private readonly design;
58
58
  private readonly hasElapsedTime?;
59
59
  private readonly hasStageTime?;
60
- private inkInstance;
60
+ private readonly inkInstance;
61
61
  private readonly postStagesBlock?;
62
62
  private readonly preStagesBlock?;
63
63
  private readonly stages;
64
64
  private readonly stageSpecificBlock?;
65
- private stageTracker;
65
+ private readonly stageTracker;
66
66
  private stopped;
67
67
  private readonly timerUnit?;
68
68
  private readonly title?;
@@ -264,8 +264,8 @@ export class MultiStageOutput {
264
264
  // Instead of refactoring the components to take a boolean, we pass in a placeholder Error,
265
265
  // which, gives us the flexibility in the future to pass in an actual Error if we want
266
266
  const error = finalStatus === 'failed' ? new Error('Error') : undefined;
267
- const stagesInput = { ...this.generateStagesInput(), ...(error ? { error } : {}) };
268
- this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput }));
267
+ const stagesInput = { ...this.generateStagesInput({ compactionLevel: 0 }), ...(error ? { error } : {}) };
268
+ this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput, compactionLevel: 0 }));
269
269
  this.inkInstance?.unmount();
270
270
  }
271
271
  [Symbol.dispose]() {
@@ -289,6 +289,7 @@ export class MultiStageOutput {
289
289
  return {
290
290
  color: info.color,
291
291
  isBold: info.bold,
292
+ neverCollapse: info.neverCollapse,
292
293
  type: info.type,
293
294
  value: formattedData,
294
295
  ...(info.type === 'message' ? {} : { label: info.label }),
@@ -297,8 +298,10 @@ export class MultiStageOutput {
297
298
  }) ?? []);
298
299
  }
299
300
  /** shared method to populate everything needed for Stages cmp */
300
- generateStagesInput() {
301
+ generateStagesInput(opts) {
302
+ const { compactionLevel } = opts ?? {};
301
303
  return {
304
+ compactionLevel,
302
305
  design: this.design,
303
306
  hasElapsedTime: this.hasElapsedTime,
304
307
  hasStageTime: this.hasStageTime,
@@ -1,11 +1,18 @@
1
- export type StageStatus = 'pending' | 'current' | 'completed' | 'skipped' | 'failed';
1
+ export type StageStatus = 'aborted' | 'async' | 'completed' | 'current' | 'failed' | 'paused' | 'pending' | 'skipped';
2
2
  export declare class StageTracker {
3
+ private stages;
3
4
  current: string | undefined;
4
5
  private map;
5
6
  private markers;
6
7
  constructor(stages: readonly string[] | string[]);
8
+ get size(): number;
7
9
  entries(): IterableIterator<[string, StageStatus]>;
8
10
  get(stage: string): StageStatus | undefined;
11
+ getCurrent(): {
12
+ stage: string;
13
+ status: StageStatus;
14
+ } | undefined;
15
+ indexOf(stage: string): number;
9
16
  refresh(nextStage: string, opts?: {
10
17
  finalStatus?: StageStatus;
11
18
  bypassStatus?: StageStatus;
@@ -1,17 +1,33 @@
1
1
  import { Performance } from '@oclif/core/performance';
2
2
  export class StageTracker {
3
+ stages;
3
4
  current;
4
5
  map = new Map();
5
6
  markers = new Map();
6
7
  constructor(stages) {
8
+ this.stages = stages;
7
9
  this.map = new Map(stages.map((stage) => [stage, 'pending']));
8
10
  }
11
+ get size() {
12
+ return this.map.size;
13
+ }
9
14
  entries() {
10
15
  return this.map.entries();
11
16
  }
12
17
  get(stage) {
13
18
  return this.map.get(stage);
14
19
  }
20
+ getCurrent() {
21
+ if (this.current) {
22
+ return {
23
+ stage: this.current,
24
+ status: this.map.get(this.current),
25
+ };
26
+ }
27
+ }
28
+ indexOf(stage) {
29
+ return this.stages.indexOf(stage);
30
+ }
15
31
  refresh(nextStage, opts) {
16
32
  const stages = [...this.map.keys()];
17
33
  for (const stage of stages) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/multi-stage-output",
3
3
  "description": "Terminal output for oclif commands with multiple stages",
4
- "version": "0.4.2",
4
+ "version": "0.4.3-dev.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/multi-stage-output/issues",
7
7
  "dependencies": {