@oclif/multi-stage-output 0.5.7 → 0.6.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.
- package/lib/components/stages.js +10 -10
- package/lib/multi-stage-output.d.ts +67 -32
- package/lib/multi-stage-output.js +118 -88
- package/lib/stage-tracker.d.ts +8 -7
- package/lib/stage-tracker.js +34 -16
- package/package.json +2 -3
package/lib/components/stages.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getLogger } from '@oclif/core/logger';
|
|
2
|
-
import { capitalCase } from 'change-case';
|
|
3
2
|
import { Box, Text, useStdout } from 'ink';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import wrapAnsi from 'wrap-ansi';
|
|
@@ -65,19 +64,19 @@ function CompactStage({ design, direction = 'row', error, stage, stageSpecificBl
|
|
|
65
64
|
if (status !== 'current')
|
|
66
65
|
return false;
|
|
67
66
|
return (React.createElement(Box, { flexDirection: direction },
|
|
68
|
-
React.createElement(SpinnerOrError, { error: error, label: `[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${
|
|
67
|
+
React.createElement(SpinnerOrError, { error: error, label: `[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${stage}`, type: design.spinners.stage, design: design }),
|
|
69
68
|
stageSpecificBlock && stageSpecificBlock.length > 0 && (React.createElement(Box, { flexDirection: "column" },
|
|
70
69
|
React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))));
|
|
71
70
|
}
|
|
72
71
|
function Stage({ design, error, stage, status, }) {
|
|
73
72
|
return (React.createElement(Box, { flexWrap: "wrap" },
|
|
74
|
-
(status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label:
|
|
73
|
+
(status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: stage, type: design.spinners.stage, design: design })),
|
|
75
74
|
status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped },
|
|
76
75
|
React.createElement(Text, { color: "dim" },
|
|
77
|
-
|
|
76
|
+
stage,
|
|
78
77
|
" - Skipped"))),
|
|
79
78
|
status !== 'skipped' && status !== 'failed' && status !== 'current' && (React.createElement(Icon, { icon: design.icons[status] },
|
|
80
|
-
React.createElement(Text, null,
|
|
79
|
+
React.createElement(Text, null, stage)))));
|
|
81
80
|
}
|
|
82
81
|
function StageEntries({ compactionLevel, design, error, hasStageTime, stageSpecificBlock, stageTracker, timerUnit, }) {
|
|
83
82
|
return (React.createElement(React.Fragment, null, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" },
|
|
@@ -87,7 +86,7 @@ function StageEntries({ compactionLevel, design, error, hasStageTime, stageSpeci
|
|
|
87
86
|
React.createElement(CompactStage, { stage: stage, status: status, design: design, error: error, stageSpecificBlock: stageSpecificBlock, stageTracker: stageTracker, direction: compactionLevel >= 6 ? 'row' : 'column' })),
|
|
88
87
|
status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, { display: compactionLevel === 0 ? 'flex' : status === 'current' ? 'flex' : 'none' },
|
|
89
88
|
React.createElement(Text, null, " "),
|
|
90
|
-
React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))),
|
|
89
|
+
React.createElement(Timer, { color: "dim", isStopped: status === 'completed' || status === 'paused', unit: timerUnit })))),
|
|
91
90
|
compactionLevel === 0 &&
|
|
92
91
|
stageSpecificBlock &&
|
|
93
92
|
stageSpecificBlock.length > 0 &&
|
|
@@ -209,8 +208,8 @@ export function determineCompactionLevel({ design = constructDesignParams(), has
|
|
|
209
208
|
1;
|
|
210
209
|
let cLevel = 0;
|
|
211
210
|
const levels = [
|
|
212
|
-
// 1: only
|
|
213
|
-
(remainingHeight) => remainingHeight - stagesHeight + 1,
|
|
211
|
+
// 1: only current stages, with stage specific info nested under the stage
|
|
212
|
+
(remainingHeight) => remainingHeight - stagesHeight + Math.max(stageTracker.current.length, 1),
|
|
214
213
|
// 2: hide the elapsed time
|
|
215
214
|
(remainingHeight) => remainingHeight - 1,
|
|
216
215
|
// 3: hide the title (subtract 1 for title and 1 for paddingBottom)
|
|
@@ -233,7 +232,8 @@ export function determineCompactionLevel({ design = constructDesignParams(), has
|
|
|
233
232
|
}
|
|
234
233
|
// It's possible that the collapsed stage might extend beyond the terminal width.
|
|
235
234
|
// If so, we need to bump the compaction level up to 7 so that the stage specific info is hidden
|
|
236
|
-
if (cLevel === 6 &&
|
|
235
|
+
if (cLevel === 6 &&
|
|
236
|
+
stageTracker.current.map((c) => calculateWidthOfCompactStage(c)).reduce((acc, width) => acc + width, 0) >= columns) {
|
|
237
237
|
cLevel = 7;
|
|
238
238
|
}
|
|
239
239
|
return {
|
|
@@ -329,7 +329,7 @@ export function Stages({ compactionLevel, design = constructDesignParams(), erro
|
|
|
329
329
|
React.createElement(ErrorBoundary, { getFallbackText: () => preStages.map((s) => (s.label ? `${s.label}: ${s.value}` : s.value)).join('\n') },
|
|
330
330
|
React.createElement(Infos, { design: design, error: error, keyValuePairs: preStages })))),
|
|
331
331
|
React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingBottom: padding },
|
|
332
|
-
React.createElement(ErrorBoundary, { getFallbackText: () => stageTracker.current ?? 'unknown' },
|
|
332
|
+
React.createElement(ErrorBoundary, { getFallbackText: () => stageTracker.current[0] ?? 'unknown' },
|
|
333
333
|
React.createElement(StageEntries, { compactionLevel: actualLevelOfCompaction, design: design, error: error, hasStageTime: hasStageTime, stageSpecificBlock: stageSpecific, stageTracker: stageTracker, timerUnit: timerUnit }))),
|
|
334
334
|
postStages && postStages.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1 },
|
|
335
335
|
React.createElement(ErrorBoundary, { getFallbackText: () => postStages.map((s) => (s.label ? `${s.label}: ${s.value}` : s.value)).join('\n') },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Instance } from 'ink';
|
|
2
|
+
import { FormattedKeyValue, InfoBlock, KeyValuePair, SimpleMessage, StageInfoBlock, StagesProps } from './components/stages.js';
|
|
3
|
+
import { Design, RequiredDesign } from './design.js';
|
|
4
|
+
import { StageStatus, StageTracker } from './stage-tracker.js';
|
|
4
5
|
export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
|
|
5
6
|
/**
|
|
6
7
|
* Stages to render.
|
|
@@ -51,26 +52,72 @@ export type MultiStageOutputOptions<T extends Record<string, unknown>> = {
|
|
|
51
52
|
*/
|
|
52
53
|
readonly jsonEnabled: boolean;
|
|
53
54
|
};
|
|
54
|
-
|
|
55
|
-
private readonly ciInstance;
|
|
55
|
+
declare class CIMultiStageOutput<T extends Record<string, unknown>> {
|
|
56
56
|
private data?;
|
|
57
57
|
private readonly design;
|
|
58
58
|
private readonly hasElapsedTime?;
|
|
59
59
|
private readonly hasStageTime?;
|
|
60
|
-
private
|
|
60
|
+
private lastUpdateTime;
|
|
61
|
+
private readonly messageTimeout;
|
|
61
62
|
private readonly postStagesBlock?;
|
|
62
63
|
private readonly preStagesBlock?;
|
|
64
|
+
private readonly seenStages;
|
|
63
65
|
private readonly stages;
|
|
64
66
|
private readonly stageSpecificBlock?;
|
|
65
|
-
private readonly
|
|
66
|
-
private
|
|
67
|
-
private readonly timerUnit
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
private readonly startTime;
|
|
68
|
+
private readonly startTimes;
|
|
69
|
+
private readonly timerUnit;
|
|
70
|
+
constructor({ data, design, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>);
|
|
71
|
+
stop(stageTracker: StageTracker): void;
|
|
72
|
+
update(stageTracker: StageTracker, data?: Partial<T>): void;
|
|
73
|
+
private printInfo;
|
|
74
|
+
}
|
|
75
|
+
declare class MultiStageOutputBase<T extends Record<string, unknown>> implements Disposable {
|
|
76
|
+
protected readonly ciInstance: CIMultiStageOutput<T> | undefined;
|
|
77
|
+
protected data?: Partial<T>;
|
|
78
|
+
protected readonly design: RequiredDesign;
|
|
79
|
+
protected readonly hasElapsedTime?: boolean;
|
|
80
|
+
protected readonly hasStageTime?: boolean;
|
|
81
|
+
protected readonly inkInstance: Instance | undefined;
|
|
82
|
+
protected readonly postStagesBlock?: InfoBlock<T>;
|
|
83
|
+
protected readonly preStagesBlock?: InfoBlock<T>;
|
|
84
|
+
protected readonly stages: readonly string[] | string[];
|
|
85
|
+
protected readonly stageSpecificBlock?: StageInfoBlock<T>;
|
|
86
|
+
protected readonly stageTracker: StageTracker;
|
|
87
|
+
protected stopped: boolean;
|
|
88
|
+
protected readonly timerUnit?: 'ms' | 's';
|
|
89
|
+
protected readonly title?: string;
|
|
90
|
+
constructor({ data, design, jsonEnabled, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }: MultiStageOutputOptions<T>, allowParallelTasks?: boolean);
|
|
70
91
|
/**
|
|
71
92
|
* Stop multi-stage output from running with a failed status.
|
|
72
93
|
*/
|
|
73
94
|
error(): void;
|
|
95
|
+
protected formatKeyValuePairs(infoBlock: InfoBlock<T> | StageInfoBlock<T> | undefined): FormattedKeyValue[];
|
|
96
|
+
/** shared method to populate everything needed for Stages cmp */
|
|
97
|
+
protected generateStagesInput(opts?: {
|
|
98
|
+
compactionLevel?: number;
|
|
99
|
+
}): StagesProps;
|
|
100
|
+
protected rerender(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Stop multi-stage output from running.
|
|
103
|
+
*
|
|
104
|
+
* The stage currently running will be changed to the provided `finalStatus`.
|
|
105
|
+
*
|
|
106
|
+
* @param finalStatus - The status to set the current stage to.
|
|
107
|
+
* @returns void
|
|
108
|
+
*/
|
|
109
|
+
stop(finalStatus?: StageStatus): void;
|
|
110
|
+
[Symbol.dispose](): void;
|
|
111
|
+
/**
|
|
112
|
+
* Updates the data of the component.
|
|
113
|
+
*
|
|
114
|
+
* @param data - The partial data object to update the component's data with.
|
|
115
|
+
* @returns void
|
|
116
|
+
*/
|
|
117
|
+
updateData(data: Partial<T>): void;
|
|
118
|
+
}
|
|
119
|
+
export declare class MultiStageOutput<T extends Record<string, unknown>> extends MultiStageOutputBase<T> {
|
|
120
|
+
constructor(options: MultiStageOutputOptions<T>);
|
|
74
121
|
/**
|
|
75
122
|
* Go to a stage, marking any stages in between the current stage and the provided stage as completed.
|
|
76
123
|
*
|
|
@@ -102,26 +149,14 @@ export declare class MultiStageOutput<T extends Record<string, unknown>> impleme
|
|
|
102
149
|
* @returns void
|
|
103
150
|
*/
|
|
104
151
|
skipTo(stage: string, data?: Partial<T>): void;
|
|
105
|
-
/**
|
|
106
|
-
* Stop multi-stage output from running.
|
|
107
|
-
*
|
|
108
|
-
* The stage currently running will be changed to the provided `finalStatus`.
|
|
109
|
-
*
|
|
110
|
-
* @param finalStatus - The status to set the current stage to.
|
|
111
|
-
* @returns void
|
|
112
|
-
*/
|
|
113
|
-
stop(finalStatus?: StageStatus): void;
|
|
114
|
-
[Symbol.dispose](): void;
|
|
115
|
-
/**
|
|
116
|
-
* Updates the data of the component.
|
|
117
|
-
*
|
|
118
|
-
* @param data - The partial data object to update the component's data with.
|
|
119
|
-
* @returns void
|
|
120
|
-
*/
|
|
121
|
-
updateData(data: Partial<T>): void;
|
|
122
|
-
private formatKeyValuePairs;
|
|
123
|
-
/** shared method to populate everything needed for Stages cmp */
|
|
124
|
-
private generateStagesInput;
|
|
125
|
-
private rerender;
|
|
126
152
|
private update;
|
|
127
153
|
}
|
|
154
|
+
export declare class ParallelMultiStageOutput<T extends Record<string, unknown>> extends MultiStageOutputBase<T> {
|
|
155
|
+
constructor(options: MultiStageOutputOptions<T>);
|
|
156
|
+
pauseStage(stage: string, data?: Partial<T>): void;
|
|
157
|
+
resumeStage(stage: string, data?: Partial<T>): void;
|
|
158
|
+
startStage(stage: string, data?: Partial<T>): void;
|
|
159
|
+
stopStage(stage: string, data?: Partial<T>): void;
|
|
160
|
+
private update;
|
|
161
|
+
}
|
|
162
|
+
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ux } from '@oclif/core/ux';
|
|
2
|
-
import { capitalCase } from 'change-case';
|
|
3
2
|
import { render } from 'ink';
|
|
4
3
|
import { env } from 'node:process';
|
|
5
4
|
import React from 'react';
|
|
@@ -63,7 +62,7 @@ class CIMultiStageOutput {
|
|
|
63
62
|
ux.stdout(`───── ${title} ─────`);
|
|
64
63
|
ux.stdout('Stages:');
|
|
65
64
|
for (const stage of this.stages) {
|
|
66
|
-
ux.stdout(`${this.stages.indexOf(stage) + 1}. ${
|
|
65
|
+
ux.stdout(`${this.stages.indexOf(stage) + 1}. ${stage}`);
|
|
67
66
|
}
|
|
68
67
|
ux.stdout();
|
|
69
68
|
if (this.hasElapsedTime) {
|
|
@@ -99,7 +98,7 @@ class CIMultiStageOutput {
|
|
|
99
98
|
this.lastUpdateTime = Date.now();
|
|
100
99
|
if (!this.startTimes.has(stage))
|
|
101
100
|
this.startTimes.set(stage, Date.now());
|
|
102
|
-
ux.stdout(`${this.design.icons.current.figure} ${
|
|
101
|
+
ux.stdout(`${this.design.icons.current.figure} ${stage}…`);
|
|
103
102
|
this.printInfo(this.preStagesBlock, 3);
|
|
104
103
|
this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
|
|
105
104
|
this.printInfo(this.postStagesBlock, 3);
|
|
@@ -113,16 +112,16 @@ class CIMultiStageOutput {
|
|
|
113
112
|
const startTime = this.startTimes.get(stage);
|
|
114
113
|
const elapsedTime = startTime ? Date.now() - startTime : 0;
|
|
115
114
|
const displayTime = readableTime(elapsedTime, this.timerUnit);
|
|
116
|
-
ux.stdout(`${this.design.icons[status].figure} ${
|
|
115
|
+
ux.stdout(`${this.design.icons[status].figure} ${stage} (${displayTime})`);
|
|
117
116
|
this.printInfo(this.preStagesBlock, 3);
|
|
118
117
|
this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
|
|
119
118
|
this.printInfo(this.postStagesBlock, 3);
|
|
120
119
|
}
|
|
121
120
|
else if (status === 'skipped') {
|
|
122
|
-
ux.stdout(`${this.design.icons[status].figure} ${
|
|
121
|
+
ux.stdout(`${this.design.icons[status].figure} ${stage} - Skipped`);
|
|
123
122
|
}
|
|
124
123
|
else {
|
|
125
|
-
ux.stdout(`${this.design.icons[status].figure} ${
|
|
124
|
+
ux.stdout(`${this.design.icons[status].figure} ${stage}`);
|
|
126
125
|
this.printInfo(this.preStagesBlock, 3);
|
|
127
126
|
this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3);
|
|
128
127
|
this.printInfo(this.postStagesBlock, 3);
|
|
@@ -151,7 +150,7 @@ class CIMultiStageOutput {
|
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
}
|
|
154
|
-
|
|
153
|
+
class MultiStageOutputBase {
|
|
155
154
|
ciInstance;
|
|
156
155
|
data;
|
|
157
156
|
design;
|
|
@@ -166,7 +165,7 @@ export class MultiStageOutput {
|
|
|
166
165
|
stopped = false;
|
|
167
166
|
timerUnit;
|
|
168
167
|
title;
|
|
169
|
-
constructor({ data, design, jsonEnabled = false, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }) {
|
|
168
|
+
constructor({ data, design, jsonEnabled = false, postStagesBlock, preStagesBlock, showElapsedTime, showStageTime, stageSpecificBlock, stages, timerUnit, title, }, allowParallelTasks) {
|
|
170
169
|
this.data = data;
|
|
171
170
|
this.design = constructDesignParams(design);
|
|
172
171
|
this.stages = stages;
|
|
@@ -176,7 +175,7 @@ export class MultiStageOutput {
|
|
|
176
175
|
this.hasElapsedTime = showElapsedTime ?? true;
|
|
177
176
|
this.hasStageTime = showStageTime ?? true;
|
|
178
177
|
this.timerUnit = timerUnit ?? 'ms';
|
|
179
|
-
this.stageTracker = new StageTracker(stages);
|
|
178
|
+
this.stageTracker = new StageTracker(stages, { allowParallelTasks });
|
|
180
179
|
this.stageSpecificBlock = stageSpecificBlock;
|
|
181
180
|
if (jsonEnabled)
|
|
182
181
|
return;
|
|
@@ -205,6 +204,89 @@ export class MultiStageOutput {
|
|
|
205
204
|
error() {
|
|
206
205
|
this.stop('failed');
|
|
207
206
|
}
|
|
207
|
+
formatKeyValuePairs(infoBlock) {
|
|
208
|
+
return (infoBlock?.map((info) => {
|
|
209
|
+
const formattedData = info.get ? info.get(this.data) : undefined;
|
|
210
|
+
return {
|
|
211
|
+
color: info.color,
|
|
212
|
+
isBold: info.bold,
|
|
213
|
+
neverCollapse: info.neverCollapse,
|
|
214
|
+
type: info.type,
|
|
215
|
+
value: formattedData,
|
|
216
|
+
...(info.type === 'message' ? {} : { label: info.label }),
|
|
217
|
+
...('stage' in info ? { stage: info.stage } : {}),
|
|
218
|
+
};
|
|
219
|
+
}) ?? []);
|
|
220
|
+
}
|
|
221
|
+
/** shared method to populate everything needed for Stages cmp */
|
|
222
|
+
generateStagesInput(opts) {
|
|
223
|
+
const { compactionLevel } = opts ?? {};
|
|
224
|
+
return {
|
|
225
|
+
compactionLevel,
|
|
226
|
+
design: this.design,
|
|
227
|
+
hasElapsedTime: this.hasElapsedTime,
|
|
228
|
+
hasStageTime: this.hasStageTime,
|
|
229
|
+
postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock),
|
|
230
|
+
preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock),
|
|
231
|
+
stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock),
|
|
232
|
+
stageTracker: this.stageTracker,
|
|
233
|
+
timerUnit: this.timerUnit,
|
|
234
|
+
title: this.title,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
rerender() {
|
|
238
|
+
if (isInCi) {
|
|
239
|
+
this.ciInstance?.update(this.stageTracker, this.data);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this.inkInstance?.rerender(React.createElement(Stages, { ...this.generateStagesInput() }));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Stop multi-stage output from running.
|
|
247
|
+
*
|
|
248
|
+
* The stage currently running will be changed to the provided `finalStatus`.
|
|
249
|
+
*
|
|
250
|
+
* @param finalStatus - The status to set the current stage to.
|
|
251
|
+
* @returns void
|
|
252
|
+
*/
|
|
253
|
+
stop(finalStatus = 'completed') {
|
|
254
|
+
if (this.stopped)
|
|
255
|
+
return;
|
|
256
|
+
this.stopped = true;
|
|
257
|
+
this.stageTracker.stop(this.stageTracker.current[0] ?? this.stages[0], finalStatus);
|
|
258
|
+
if (isInCi) {
|
|
259
|
+
this.ciInstance?.stop(this.stageTracker);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// The underlying components expect an Error, although they don't currently use anything on the error - they check if it exists.
|
|
263
|
+
// Instead of refactoring the components to take a boolean, we pass in a placeholder Error,
|
|
264
|
+
// which, gives us the flexibility in the future to pass in an actual Error if we want
|
|
265
|
+
const error = finalStatus === 'failed' ? new Error('Error') : undefined;
|
|
266
|
+
const stagesInput = { ...this.generateStagesInput({ compactionLevel: 0 }), ...(error ? { error } : {}) };
|
|
267
|
+
this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput, compactionLevel: 0 }));
|
|
268
|
+
this.inkInstance?.unmount();
|
|
269
|
+
}
|
|
270
|
+
[Symbol.dispose]() {
|
|
271
|
+
this.inkInstance?.unmount();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Updates the data of the component.
|
|
275
|
+
*
|
|
276
|
+
* @param data - The partial data object to update the component's data with.
|
|
277
|
+
* @returns void
|
|
278
|
+
*/
|
|
279
|
+
updateData(data) {
|
|
280
|
+
if (this.stopped)
|
|
281
|
+
return;
|
|
282
|
+
this.data = { ...this.data, ...data };
|
|
283
|
+
this.rerender();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export class MultiStageOutput extends MultiStageOutputBase {
|
|
287
|
+
constructor(options) {
|
|
288
|
+
super(options);
|
|
289
|
+
}
|
|
208
290
|
/**
|
|
209
291
|
* Go to a stage, marking any stages in between the current stage and the provided stage as completed.
|
|
210
292
|
*
|
|
@@ -223,7 +305,7 @@ export class MultiStageOutput {
|
|
|
223
305
|
if (!this.stages.includes(stage))
|
|
224
306
|
return;
|
|
225
307
|
// prevent going to a previous stage
|
|
226
|
-
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0]))
|
|
308
|
+
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current[0] ?? this.stages[0]))
|
|
227
309
|
return;
|
|
228
310
|
this.update(stage, 'completed', data);
|
|
229
311
|
}
|
|
@@ -236,7 +318,7 @@ export class MultiStageOutput {
|
|
|
236
318
|
next(data) {
|
|
237
319
|
if (this.stopped)
|
|
238
320
|
return;
|
|
239
|
-
const nextStageIndex = this.stages.indexOf(this.stageTracker.current ?? this.stages[0]) + 1;
|
|
321
|
+
const nextStageIndex = this.stages.indexOf(this.stageTracker.current[0] ?? this.stages[0]) + 1;
|
|
240
322
|
if (nextStageIndex < this.stages.length) {
|
|
241
323
|
this.update(this.stages[nextStageIndex], 'completed', data);
|
|
242
324
|
}
|
|
@@ -259,93 +341,41 @@ export class MultiStageOutput {
|
|
|
259
341
|
if (!this.stages.includes(stage))
|
|
260
342
|
return;
|
|
261
343
|
// prevent going to a previous stage
|
|
262
|
-
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0]))
|
|
344
|
+
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current[0] ?? this.stages[0]))
|
|
263
345
|
return;
|
|
264
346
|
this.update(stage, 'skipped', data);
|
|
265
347
|
}
|
|
266
|
-
|
|
267
|
-
* Stop multi-stage output from running.
|
|
268
|
-
*
|
|
269
|
-
* The stage currently running will be changed to the provided `finalStatus`.
|
|
270
|
-
*
|
|
271
|
-
* @param finalStatus - The status to set the current stage to.
|
|
272
|
-
* @returns void
|
|
273
|
-
*/
|
|
274
|
-
stop(finalStatus = 'completed') {
|
|
275
|
-
if (this.stopped)
|
|
276
|
-
return;
|
|
277
|
-
this.stopped = true;
|
|
278
|
-
this.stageTracker.refresh(this.stageTracker.current ?? this.stages[0], {
|
|
279
|
-
finalStatus,
|
|
280
|
-
});
|
|
281
|
-
if (isInCi) {
|
|
282
|
-
this.ciInstance?.stop(this.stageTracker);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// The underlying components expect an Error, although they don't currently use anything on the error - they check if it exists.
|
|
286
|
-
// Instead of refactoring the components to take a boolean, we pass in a placeholder Error,
|
|
287
|
-
// which, gives us the flexibility in the future to pass in an actual Error if we want
|
|
288
|
-
const error = finalStatus === 'failed' ? new Error('Error') : undefined;
|
|
289
|
-
const stagesInput = { ...this.generateStagesInput({ compactionLevel: 0 }), ...(error ? { error } : {}) };
|
|
290
|
-
this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput, compactionLevel: 0 }));
|
|
291
|
-
this.inkInstance?.unmount();
|
|
292
|
-
}
|
|
293
|
-
[Symbol.dispose]() {
|
|
294
|
-
this.inkInstance?.unmount();
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Updates the data of the component.
|
|
298
|
-
*
|
|
299
|
-
* @param data - The partial data object to update the component's data with.
|
|
300
|
-
* @returns void
|
|
301
|
-
*/
|
|
302
|
-
updateData(data) {
|
|
303
|
-
if (this.stopped)
|
|
304
|
-
return;
|
|
348
|
+
update(stage, bypassStatus, data) {
|
|
305
349
|
this.data = { ...this.data, ...data };
|
|
350
|
+
this.stageTracker.refresh(stage, { bypassStatus });
|
|
306
351
|
this.rerender();
|
|
307
352
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
color: info.color,
|
|
313
|
-
isBold: info.bold,
|
|
314
|
-
neverCollapse: info.neverCollapse,
|
|
315
|
-
type: info.type,
|
|
316
|
-
value: formattedData,
|
|
317
|
-
...(info.type === 'message' ? {} : { label: info.label }),
|
|
318
|
-
...('stage' in info ? { stage: info.stage } : {}),
|
|
319
|
-
};
|
|
320
|
-
}) ?? []);
|
|
353
|
+
}
|
|
354
|
+
export class ParallelMultiStageOutput extends MultiStageOutputBase {
|
|
355
|
+
constructor(options) {
|
|
356
|
+
super(options, true);
|
|
321
357
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const { compactionLevel } = opts ?? {};
|
|
325
|
-
return {
|
|
326
|
-
compactionLevel,
|
|
327
|
-
design: this.design,
|
|
328
|
-
hasElapsedTime: this.hasElapsedTime,
|
|
329
|
-
hasStageTime: this.hasStageTime,
|
|
330
|
-
postStagesBlock: this.formatKeyValuePairs(this.postStagesBlock),
|
|
331
|
-
preStagesBlock: this.formatKeyValuePairs(this.preStagesBlock),
|
|
332
|
-
stageSpecificBlock: this.formatKeyValuePairs(this.stageSpecificBlock),
|
|
333
|
-
stageTracker: this.stageTracker,
|
|
334
|
-
timerUnit: this.timerUnit,
|
|
335
|
-
title: this.title,
|
|
336
|
-
};
|
|
358
|
+
pauseStage(stage, data) {
|
|
359
|
+
this.update(stage, 'paused', data);
|
|
337
360
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.ciInstance?.update(this.stageTracker, this.data);
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
this.inkInstance?.rerender(React.createElement(Stages, { ...this.generateStagesInput() }));
|
|
344
|
-
}
|
|
361
|
+
resumeStage(stage, data) {
|
|
362
|
+
this.update(stage, 'current', data);
|
|
345
363
|
}
|
|
346
|
-
|
|
364
|
+
startStage(stage, data) {
|
|
365
|
+
this.update(stage, 'current', data);
|
|
366
|
+
}
|
|
367
|
+
stopStage(stage, data) {
|
|
368
|
+
this.update(stage, 'completed', data);
|
|
369
|
+
}
|
|
370
|
+
update(stage, status, data) {
|
|
371
|
+
if (this.stopped)
|
|
372
|
+
return;
|
|
373
|
+
if (!this.stages.includes(stage))
|
|
374
|
+
return;
|
|
375
|
+
if (this.stageTracker.get(stage) === 'completed')
|
|
376
|
+
return;
|
|
347
377
|
this.data = { ...this.data, ...data };
|
|
348
|
-
this.stageTracker.
|
|
378
|
+
this.stageTracker.update(stage, status);
|
|
349
379
|
this.rerender();
|
|
350
380
|
}
|
|
351
381
|
}
|
package/lib/stage-tracker.d.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
export type StageStatus = 'aborted' | 'async' | 'completed' | 'current' | 'failed' | 'paused' | 'pending' | 'skipped' | 'warning';
|
|
2
2
|
export declare class StageTracker {
|
|
3
3
|
private stages;
|
|
4
|
-
current: string
|
|
4
|
+
current: string[];
|
|
5
|
+
private allowParallelTasks;
|
|
5
6
|
private map;
|
|
6
7
|
private markers;
|
|
7
|
-
constructor(stages: readonly string[] | string[]
|
|
8
|
+
constructor(stages: readonly string[] | string[], opts?: {
|
|
9
|
+
allowParallelTasks?: boolean;
|
|
10
|
+
});
|
|
8
11
|
get size(): number;
|
|
9
12
|
entries(): IterableIterator<[string, StageStatus]>;
|
|
10
13
|
get(stage: string): StageStatus | undefined;
|
|
11
|
-
getCurrent(): {
|
|
12
|
-
stage: string;
|
|
13
|
-
status: StageStatus;
|
|
14
|
-
} | undefined;
|
|
15
14
|
indexOf(stage: string): number;
|
|
16
15
|
refresh(nextStage: string, opts?: {
|
|
17
16
|
finalStatus?: StageStatus;
|
|
18
17
|
bypassStatus?: StageStatus;
|
|
19
18
|
}): void;
|
|
20
19
|
set(stage: string, status: StageStatus): void;
|
|
20
|
+
stop(currentStage: string, finalStatus: StageStatus): void;
|
|
21
|
+
update(stage: string, status: StageStatus): void;
|
|
21
22
|
values(): IterableIterator<StageStatus>;
|
|
22
|
-
private
|
|
23
|
+
private stopStage;
|
|
23
24
|
}
|
package/lib/stage-tracker.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Performance } from '@oclif/core/performance';
|
|
2
2
|
export class StageTracker {
|
|
3
3
|
stages;
|
|
4
|
-
current;
|
|
4
|
+
current = [];
|
|
5
|
+
allowParallelTasks;
|
|
5
6
|
map = new Map();
|
|
6
7
|
markers = new Map();
|
|
7
|
-
constructor(stages) {
|
|
8
|
+
constructor(stages, opts) {
|
|
8
9
|
this.stages = stages;
|
|
9
10
|
this.map = new Map(stages.map((stage) => [stage, 'pending']));
|
|
11
|
+
this.allowParallelTasks = opts?.allowParallelTasks ?? false;
|
|
10
12
|
}
|
|
11
13
|
get size() {
|
|
12
14
|
return this.map.size;
|
|
@@ -17,14 +19,6 @@ export class StageTracker {
|
|
|
17
19
|
get(stage) {
|
|
18
20
|
return this.map.get(stage);
|
|
19
21
|
}
|
|
20
|
-
getCurrent() {
|
|
21
|
-
if (this.current) {
|
|
22
|
-
return {
|
|
23
|
-
stage: this.current,
|
|
24
|
-
status: this.map.get(this.current),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
22
|
indexOf(stage) {
|
|
29
23
|
return this.stages.indexOf(stage);
|
|
30
24
|
}
|
|
@@ -37,8 +31,7 @@ export class StageTracker {
|
|
|
37
31
|
continue;
|
|
38
32
|
// .stop() was called with a finalStatus
|
|
39
33
|
if (nextStage === stage && opts?.finalStatus) {
|
|
40
|
-
this.
|
|
41
|
-
this.stopMarker(stage);
|
|
34
|
+
this.stopStage(stage, opts.finalStatus);
|
|
42
35
|
continue;
|
|
43
36
|
}
|
|
44
37
|
// set the current stage
|
|
@@ -57,8 +50,7 @@ export class StageTracker {
|
|
|
57
50
|
}
|
|
58
51
|
// any stage before the current stage should be marked as completed (if it hasn't been marked as skipped or failed yet)
|
|
59
52
|
if (stages.indexOf(nextStage) > stages.indexOf(stage)) {
|
|
60
|
-
this.
|
|
61
|
-
this.stopMarker(stage);
|
|
53
|
+
this.stopStage(stage, 'completed');
|
|
62
54
|
continue;
|
|
63
55
|
}
|
|
64
56
|
// default to pending
|
|
@@ -67,14 +59,40 @@ export class StageTracker {
|
|
|
67
59
|
}
|
|
68
60
|
set(stage, status) {
|
|
69
61
|
if (status === 'current') {
|
|
70
|
-
this.current
|
|
62
|
+
if (!this.current.includes(stage)) {
|
|
63
|
+
this.current.push(stage);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.current = this.current.filter((s) => s !== stage);
|
|
71
68
|
}
|
|
72
69
|
this.map.set(stage, status);
|
|
73
70
|
}
|
|
71
|
+
stop(currentStage, finalStatus) {
|
|
72
|
+
if (this.allowParallelTasks) {
|
|
73
|
+
for (const [stage, status] of this.entries()) {
|
|
74
|
+
if (status === 'current') {
|
|
75
|
+
this.stopStage(stage, finalStatus);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.refresh(currentStage, { finalStatus });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
update(stage, status) {
|
|
84
|
+
if (status === 'completed' || status === 'failed' || status === 'aborted') {
|
|
85
|
+
this.stopStage(stage, status);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.set(stage, status);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
74
91
|
values() {
|
|
75
92
|
return this.map.values();
|
|
76
93
|
}
|
|
77
|
-
|
|
94
|
+
stopStage(stage, status) {
|
|
95
|
+
this.set(stage, status);
|
|
78
96
|
const marker = this.markers.get(stage);
|
|
79
97
|
if (marker && !marker.stopped) {
|
|
80
98
|
marker.stop();
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oclif/multi-stage-output",
|
|
3
3
|
"description": "Terminal output for oclif commands with multiple stages",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"author": "Salesforce",
|
|
6
6
|
"bugs": "https://github.com/oclif/multi-stage-output/issues",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@oclif/core": "^4",
|
|
9
9
|
"@types/react": "^18.3.8",
|
|
10
|
-
"change-case": "^5.4.4",
|
|
11
10
|
"cli-spinners": "^2",
|
|
12
11
|
"figures": "^6.1.0",
|
|
13
12
|
"ink": "^5.0.1",
|
|
@@ -31,7 +30,7 @@
|
|
|
31
30
|
"eslint-config-xo-react": "^0.27.0",
|
|
32
31
|
"eslint-plugin-react": "^7.36.1",
|
|
33
32
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
34
|
-
"husky": "^9.1.
|
|
33
|
+
"husky": "^9.1.6",
|
|
35
34
|
"ink-testing-library": "^4.0.0",
|
|
36
35
|
"lint-staged": "^15",
|
|
37
36
|
"mocha": "^10.7.3",
|