@testingbot/cli 1.0.6 → 1.0.8
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/README.md +25 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +79 -50
- package/dist/config/constants.d.ts +15 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +17 -0
- package/dist/index.js +2 -0
- package/dist/models/espresso_options.d.ts +11 -4
- package/dist/models/espresso_options.d.ts.map +1 -1
- package/dist/models/espresso_options.js +24 -7
- package/dist/models/maestro_options.d.ts +13 -3
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +25 -2
- package/dist/models/xcuitest_options.d.ts +11 -4
- package/dist/models/xcuitest_options.d.ts.map +1 -1
- package/dist/models/xcuitest_options.js +24 -7
- package/dist/providers/base_provider.d.ts +28 -2
- package/dist/providers/base_provider.d.ts.map +1 -1
- package/dist/providers/base_provider.js +70 -2
- package/dist/providers/espresso.d.ts +1 -0
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +82 -35
- package/dist/providers/maestro.d.ts +21 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +320 -72
- package/dist/providers/xcuitest.d.ts +1 -0
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +79 -35
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +82 -0
- package/dist/ui/spinner.d.ts +32 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +92 -0
- package/dist/ui/terminal-title.d.ts +8 -0
- package/dist/ui/terminal-title.d.ts.map +1 -0
- package/dist/ui/terminal-title.js +57 -0
- package/dist/upload.d.ts +4 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +70 -12
- package/dist/utils/connectivity.js +5 -3
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -0
- package/package.json +5 -3
|
@@ -50,12 +50,21 @@ const utils_1 = __importDefault(require("../utils"));
|
|
|
50
50
|
const file_type_detector_1 = require("../utils/file-type-detector");
|
|
51
51
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
52
52
|
const base_provider_1 = __importDefault(require("./base_provider"));
|
|
53
|
+
const terminal_title_1 = require("../ui/terminal-title");
|
|
54
|
+
const constants_1 = require("../config/constants");
|
|
55
|
+
const FLOW_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
56
|
+
const FLOW_ANIMATION_MS = 120;
|
|
53
57
|
class Maestro extends base_provider_1.default {
|
|
54
58
|
URL = 'https://api.testingbot.com/v1/app-automate/maestro';
|
|
55
59
|
detectedPlatform = undefined;
|
|
56
60
|
socket = null;
|
|
57
61
|
updateServer = null;
|
|
58
62
|
updateKey = null;
|
|
63
|
+
socketFallbackWarned = false;
|
|
64
|
+
flowAnimationFrame = 0;
|
|
65
|
+
flowAnimationTimer = null;
|
|
66
|
+
latestFlows = [];
|
|
67
|
+
latestDisplayedLineCount = 0;
|
|
59
68
|
constructor(credentials, options) {
|
|
60
69
|
super(credentials, options);
|
|
61
70
|
}
|
|
@@ -88,6 +97,13 @@ class Maestro extends base_provider_1.default {
|
|
|
88
97
|
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
89
98
|
}),
|
|
90
99
|
];
|
|
100
|
+
if (this.options.configFile) {
|
|
101
|
+
fileChecks.push(node_fs_1.default.promises
|
|
102
|
+
.access(this.options.configFile, node_fs_1.default.constants.R_OK)
|
|
103
|
+
.catch(() => {
|
|
104
|
+
throw new testingbot_error_1.default(`Specified config file does not exist: ${this.options.configFile}`);
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
91
107
|
// Check if all flows paths exist (can be files, directories or glob patterns)
|
|
92
108
|
for (const flowsPath of this.options.flows) {
|
|
93
109
|
const isGlobPattern = flowsPath.includes('*') ||
|
|
@@ -172,20 +188,28 @@ class Maestro extends base_provider_1.default {
|
|
|
172
188
|
return { success: true, runs: [] };
|
|
173
189
|
}
|
|
174
190
|
try {
|
|
191
|
+
(0, terminal_title_1.setTitle)('maestro');
|
|
175
192
|
// Quick connectivity check before starting uploads
|
|
176
193
|
await this.ensureConnectivity();
|
|
177
194
|
// Detect platform from file content if not explicitly provided
|
|
178
195
|
if (!this.options.platformName) {
|
|
179
196
|
this.detectedPlatform = await this.detectPlatform();
|
|
180
197
|
}
|
|
198
|
+
(0, terminal_title_1.setTitle)('maestro · uploading app');
|
|
181
199
|
await this.uploadApp();
|
|
182
200
|
if (!this.options.quiet) {
|
|
183
201
|
logger_1.default.info('Uploading Maestro Flows');
|
|
184
202
|
}
|
|
203
|
+
(0, terminal_title_1.setTitle)('maestro · uploading flows');
|
|
185
204
|
await this.uploadFlows();
|
|
205
|
+
if (this.options.tunnel && this.options.async) {
|
|
206
|
+
throw new testingbot_error_1.default('Cannot use --tunnel with --async mode. The tunnel would close when the CLI exits. Use a standalone tunnel instead.');
|
|
207
|
+
}
|
|
208
|
+
await this.startTunnel();
|
|
186
209
|
if (!this.options.quiet) {
|
|
187
210
|
logger_1.default.info('Running Maestro Tests');
|
|
188
211
|
}
|
|
212
|
+
(0, terminal_title_1.setTitle)('maestro · queued');
|
|
189
213
|
await this.runTests();
|
|
190
214
|
if (this.options.async) {
|
|
191
215
|
if (!this.options.quiet) {
|
|
@@ -204,12 +228,17 @@ class Maestro extends base_provider_1.default {
|
|
|
204
228
|
// Clean up
|
|
205
229
|
this.disconnectFromUpdateServer();
|
|
206
230
|
this.removeSignalHandlers();
|
|
231
|
+
await this.stopTunnel();
|
|
207
232
|
return result;
|
|
208
233
|
}
|
|
209
234
|
catch (error) {
|
|
210
235
|
// Clean up on error
|
|
236
|
+
this.spinner.stop();
|
|
237
|
+
this.stopFlowAnimation();
|
|
211
238
|
this.disconnectFromUpdateServer();
|
|
212
239
|
this.removeSignalHandlers();
|
|
240
|
+
await this.stopTunnel();
|
|
241
|
+
(0, terminal_title_1.setTitle)('maestro · ✘ error');
|
|
213
242
|
logger_1.default.error(error instanceof Error ? error.message : error);
|
|
214
243
|
// Display the cause if available
|
|
215
244
|
if (error instanceof Error && error.cause) {
|
|
@@ -224,7 +253,7 @@ class Maestro extends base_provider_1.default {
|
|
|
224
253
|
async uploadApp() {
|
|
225
254
|
let appPath = this.options.app;
|
|
226
255
|
const ext = node_path_1.default.extname(appPath).toLowerCase();
|
|
227
|
-
let
|
|
256
|
+
let tempZipDir = null;
|
|
228
257
|
// If .app bundle (directory), zip it first
|
|
229
258
|
if (ext === '.app') {
|
|
230
259
|
const stat = await node_fs_1.default.promises.stat(appPath);
|
|
@@ -232,8 +261,9 @@ class Maestro extends base_provider_1.default {
|
|
|
232
261
|
if (!this.options.quiet) {
|
|
233
262
|
logger_1.default.info('Zipping .app bundle for upload');
|
|
234
263
|
}
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
const zipped = await this.zipAppBundle(appPath);
|
|
265
|
+
tempZipDir = zipped.tmpDir;
|
|
266
|
+
appPath = zipped.zipPath;
|
|
237
267
|
}
|
|
238
268
|
}
|
|
239
269
|
try {
|
|
@@ -277,9 +307,12 @@ class Maestro extends base_provider_1.default {
|
|
|
277
307
|
return true;
|
|
278
308
|
}
|
|
279
309
|
finally {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
310
|
+
if (tempZipDir) {
|
|
311
|
+
await node_fs_1.default.promises
|
|
312
|
+
.rm(tempZipDir, { recursive: true, force: true })
|
|
313
|
+
.catch((err) => {
|
|
314
|
+
logger_1.default.debug(`Failed to clean up temporary app zip dir ${tempZipDir}: ${err instanceof Error ? err.message : err}`);
|
|
315
|
+
});
|
|
283
316
|
}
|
|
284
317
|
}
|
|
285
318
|
}
|
|
@@ -293,7 +326,8 @@ class Maestro extends base_provider_1.default {
|
|
|
293
326
|
return new Promise((resolve, reject) => {
|
|
294
327
|
const output = node_fs_1.default.createWriteStream(zipPath);
|
|
295
328
|
const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
|
|
296
|
-
output.on('close', () => resolve(zipPath));
|
|
329
|
+
output.on('close', () => resolve({ zipPath, tmpDir }));
|
|
330
|
+
output.on('error', (err) => reject(err));
|
|
297
331
|
archive.on('error', (err) => reject(err));
|
|
298
332
|
archive.pipe(output);
|
|
299
333
|
// Add the .app directory with its name preserved
|
|
@@ -339,8 +373,7 @@ class Maestro extends base_provider_1.default {
|
|
|
339
373
|
if (flowsPaths.length === 1) {
|
|
340
374
|
const singlePath = flowsPaths[0];
|
|
341
375
|
const stat = await node_fs_1.default.promises.stat(singlePath).catch(() => null);
|
|
342
|
-
if (stat?.isFile() &&
|
|
343
|
-
node_path_1.default.extname(singlePath).toLowerCase() === '.zip') {
|
|
376
|
+
if (stat?.isFile() && node_path_1.default.extname(singlePath).toLowerCase() === '.zip') {
|
|
344
377
|
return null;
|
|
345
378
|
}
|
|
346
379
|
}
|
|
@@ -419,6 +452,18 @@ class Maestro extends base_provider_1.default {
|
|
|
419
452
|
}
|
|
420
453
|
}
|
|
421
454
|
}
|
|
455
|
+
// Apply --include-tags / --exclude-tags filtering: drop flow files whose
|
|
456
|
+
// frontmatter tags don't match, and drop dependencies orphaned as a result.
|
|
457
|
+
const configTags = baseDir
|
|
458
|
+
? await this.loadConfigTags(baseDir)
|
|
459
|
+
: { includeTags: undefined, excludeTags: undefined };
|
|
460
|
+
const effectiveIncludeTags = this.options.includeTags ?? configTags.includeTags;
|
|
461
|
+
const effectiveExcludeTags = this.options.excludeTags ?? configTags.excludeTags;
|
|
462
|
+
const filtered = await this.filterFlowsByTags(allFlowFiles, baseDir, effectiveIncludeTags, effectiveExcludeTags);
|
|
463
|
+
if (filtered !== allFlowFiles) {
|
|
464
|
+
allFlowFiles.length = 0;
|
|
465
|
+
allFlowFiles.push(...filtered);
|
|
466
|
+
}
|
|
422
467
|
if (!this.options.quiet) {
|
|
423
468
|
this.logIncludedFiles(allFlowFiles, baseDir);
|
|
424
469
|
// Show info about potential slow execution on specific real devices
|
|
@@ -455,7 +500,7 @@ class Maestro extends base_provider_1.default {
|
|
|
455
500
|
return true;
|
|
456
501
|
}
|
|
457
502
|
const { allFlowFiles, baseDir } = result;
|
|
458
|
-
const zipPath = await this.createFlowsZip(allFlowFiles, baseDir);
|
|
503
|
+
const { zipPath, tmpDir } = await this.createFlowsZip(allFlowFiles, baseDir);
|
|
459
504
|
try {
|
|
460
505
|
await this.upload.upload({
|
|
461
506
|
filePath: zipPath,
|
|
@@ -466,7 +511,11 @@ class Maestro extends base_provider_1.default {
|
|
|
466
511
|
});
|
|
467
512
|
}
|
|
468
513
|
finally {
|
|
469
|
-
await node_fs_1.default.promises
|
|
514
|
+
await node_fs_1.default.promises
|
|
515
|
+
.rm(tmpDir, { recursive: true, force: true })
|
|
516
|
+
.catch((err) => {
|
|
517
|
+
logger_1.default.debug(`Failed to clean up temporary flows zip dir ${tmpDir}: ${err instanceof Error ? err.message : err}`);
|
|
518
|
+
});
|
|
470
519
|
}
|
|
471
520
|
return true;
|
|
472
521
|
}
|
|
@@ -502,9 +551,14 @@ class Maestro extends base_provider_1.default {
|
|
|
502
551
|
const flowFiles = [];
|
|
503
552
|
let configPath = null;
|
|
504
553
|
let config = null;
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
554
|
+
// If a custom config file is specified, use it; otherwise check for config.yaml or config.yml
|
|
555
|
+
const configCandidates = this.options.configFile
|
|
556
|
+
? [node_path_1.default.resolve(this.options.configFile)]
|
|
557
|
+
: [
|
|
558
|
+
node_path_1.default.join(directory, 'config.yaml'),
|
|
559
|
+
node_path_1.default.join(directory, 'config.yml'),
|
|
560
|
+
];
|
|
561
|
+
for (const candidatePath of configCandidates) {
|
|
508
562
|
try {
|
|
509
563
|
const configContent = await node_fs_1.default.promises.readFile(candidatePath, 'utf-8');
|
|
510
564
|
config = yaml.load(configContent);
|
|
@@ -578,15 +632,118 @@ class Maestro extends base_provider_1.default {
|
|
|
578
632
|
*/
|
|
579
633
|
isConfigFile(filePath) {
|
|
580
634
|
const basename = node_path_1.default.basename(filePath);
|
|
581
|
-
|
|
635
|
+
if (basename === 'config.yaml' || basename === 'config.yml') {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
if (this.options.configFile) {
|
|
639
|
+
return basename === node_path_1.default.basename(this.options.configFile);
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
async readFlowTags(flowFile) {
|
|
644
|
+
try {
|
|
645
|
+
const content = await node_fs_1.default.promises.readFile(flowFile, 'utf-8');
|
|
646
|
+
const documents = [];
|
|
647
|
+
yaml.loadAll(content, (doc) => documents.push(doc));
|
|
648
|
+
for (const doc of documents) {
|
|
649
|
+
if (doc !== null && typeof doc === 'object' && !Array.isArray(doc)) {
|
|
650
|
+
const tags = doc.tags;
|
|
651
|
+
if (Array.isArray(tags)) {
|
|
652
|
+
return tags.filter((t) => typeof t === 'string');
|
|
653
|
+
}
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// ignore
|
|
660
|
+
}
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Load includeTags / excludeTags declared in the Maestro project's
|
|
665
|
+
* config.yaml (or config.yml). Returns undefined fields when no config
|
|
666
|
+
* exists or the values are not arrays.
|
|
667
|
+
*/
|
|
668
|
+
async loadConfigTags(baseDir) {
|
|
669
|
+
const candidates = this.options.configFile
|
|
670
|
+
? [node_path_1.default.resolve(this.options.configFile)]
|
|
671
|
+
: [node_path_1.default.join(baseDir, 'config.yaml'), node_path_1.default.join(baseDir, 'config.yml')];
|
|
672
|
+
for (const candidate of candidates) {
|
|
673
|
+
try {
|
|
674
|
+
const content = await node_fs_1.default.promises.readFile(candidate, 'utf-8');
|
|
675
|
+
const parsed = yaml.load(content);
|
|
676
|
+
if (parsed && typeof parsed === 'object') {
|
|
677
|
+
const include = Array.isArray(parsed.includeTags)
|
|
678
|
+
? parsed.includeTags.filter((t) => typeof t === 'string')
|
|
679
|
+
: undefined;
|
|
680
|
+
const exclude = Array.isArray(parsed.excludeTags)
|
|
681
|
+
? parsed.excludeTags.filter((t) => typeof t === 'string')
|
|
682
|
+
: undefined;
|
|
683
|
+
return { includeTags: include, excludeTags: exclude };
|
|
684
|
+
}
|
|
685
|
+
return {};
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// try next candidate
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return {};
|
|
692
|
+
}
|
|
693
|
+
async filterFlowsByTags(allFlowFiles, baseDir, includeTagsArg, excludeTagsArg) {
|
|
694
|
+
const includeTags = includeTagsArg ?? [];
|
|
695
|
+
const excludeTags = excludeTagsArg ?? [];
|
|
696
|
+
const hasInclude = includeTags.length > 0;
|
|
697
|
+
const hasExclude = excludeTags.length > 0;
|
|
698
|
+
if (!hasInclude && !hasExclude) {
|
|
699
|
+
return allFlowFiles;
|
|
700
|
+
}
|
|
701
|
+
const yamlFlows = [];
|
|
702
|
+
const configFiles = [];
|
|
703
|
+
for (const f of allFlowFiles) {
|
|
704
|
+
const ext = node_path_1.default.extname(f).toLowerCase();
|
|
705
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
706
|
+
if (this.isConfigFile(f)) {
|
|
707
|
+
configFiles.push(f);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
yamlFlows.push(f);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const keptYamlFlows = [];
|
|
715
|
+
for (const flowFile of yamlFlows) {
|
|
716
|
+
const tags = await this.readFlowTags(flowFile);
|
|
717
|
+
if (hasInclude && !tags.some((t) => includeTags.includes(t))) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (hasExclude && tags.some((t) => excludeTags.includes(t))) {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
keptYamlFlows.push(flowFile);
|
|
724
|
+
}
|
|
725
|
+
if (keptYamlFlows.length === 0) {
|
|
726
|
+
throw new testingbot_error_1.default(`No flow files match the provided tag filters (--include-tags / --exclude-tags)`);
|
|
727
|
+
}
|
|
728
|
+
const keptResolved = new Set([...keptYamlFlows, ...configFiles].map((f) => node_path_1.default.resolve(f)));
|
|
729
|
+
for (const flowFile of keptYamlFlows) {
|
|
730
|
+
const deps = await this.discoverDependencies(flowFile, baseDir || node_path_1.default.dirname(flowFile));
|
|
731
|
+
for (const dep of deps) {
|
|
732
|
+
keptResolved.add(node_path_1.default.resolve(dep));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return allFlowFiles.filter((f) => keptResolved.has(node_path_1.default.resolve(f)));
|
|
582
736
|
}
|
|
583
737
|
/**
|
|
584
738
|
* Check if a string looks like a file path (relative path with extension)
|
|
585
739
|
*/
|
|
586
740
|
looksLikePath(value) {
|
|
587
|
-
// Must be a relative path (starts with . or contains
|
|
588
|
-
const isRelative = value.startsWith('./') ||
|
|
589
|
-
|
|
741
|
+
// Must be a relative path (starts with . or contains a path separator)
|
|
742
|
+
const isRelative = value.startsWith('./') ||
|
|
743
|
+
value.startsWith('../') ||
|
|
744
|
+
value.startsWith('.\\') ||
|
|
745
|
+
value.startsWith('..\\');
|
|
746
|
+
const hasPathSeparator = value.includes('/') || value.includes('\\');
|
|
590
747
|
// Must have a file extension
|
|
591
748
|
const hasExtension = /\.[a-zA-Z0-9]+$/.test(value);
|
|
592
749
|
// Exclude URLs
|
|
@@ -981,7 +1138,8 @@ class Maestro extends base_provider_1.default {
|
|
|
981
1138
|
return new Promise((resolve, reject) => {
|
|
982
1139
|
const output = node_fs_1.default.createWriteStream(zipPath);
|
|
983
1140
|
const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
|
|
984
|
-
output.on('close', () => resolve(zipPath));
|
|
1141
|
+
output.on('close', () => resolve({ zipPath, tmpDir }));
|
|
1142
|
+
output.on('error', (err) => reject(err));
|
|
985
1143
|
archive.on('error', (err) => reject(err));
|
|
986
1144
|
archive.pipe(output);
|
|
987
1145
|
// Compute effective base directory for archive paths
|
|
@@ -1037,7 +1195,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1037
1195
|
username: this.credentials.userName,
|
|
1038
1196
|
password: this.credentials.accessKey,
|
|
1039
1197
|
},
|
|
1040
|
-
timeout:
|
|
1198
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
1041
1199
|
});
|
|
1042
1200
|
// Check for version update notification
|
|
1043
1201
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -1075,7 +1233,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1075
1233
|
username: this.credentials.userName,
|
|
1076
1234
|
password: this.credentials.accessKey,
|
|
1077
1235
|
},
|
|
1078
|
-
timeout:
|
|
1236
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
1079
1237
|
});
|
|
1080
1238
|
// Check for version update notification
|
|
1081
1239
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -1091,14 +1249,15 @@ class Maestro extends base_provider_1.default {
|
|
|
1091
1249
|
}
|
|
1092
1250
|
}
|
|
1093
1251
|
async waitForCompletion() {
|
|
1094
|
-
let attempts = 0;
|
|
1095
1252
|
const startTime = Date.now();
|
|
1096
1253
|
const previousStatus = new Map();
|
|
1097
1254
|
const previousFlowStatus = new Map();
|
|
1098
1255
|
const urlDisplayed = new Set();
|
|
1099
1256
|
let flowsTableDisplayed = false;
|
|
1100
1257
|
let displayedLineCount = 0;
|
|
1101
|
-
|
|
1258
|
+
let pollInterval = this.MIN_POLL_INTERVAL_MS;
|
|
1259
|
+
let previousSignature = null;
|
|
1260
|
+
while (true) {
|
|
1102
1261
|
// Check if we're shutting down
|
|
1103
1262
|
if (this.isShuttingDown) {
|
|
1104
1263
|
throw new testingbot_error_1.default('Test run cancelled by user');
|
|
@@ -1108,6 +1267,11 @@ class Maestro extends base_provider_1.default {
|
|
|
1108
1267
|
this.activeRunIds = status.runs
|
|
1109
1268
|
.filter((run) => run.status !== 'DONE' && run.status !== 'FAILED')
|
|
1110
1269
|
.map((run) => run.id);
|
|
1270
|
+
const running = status.runs.find((r) => r.status === 'READY');
|
|
1271
|
+
if (running) {
|
|
1272
|
+
const device = running.environment?.name || running.capabilities.deviceName;
|
|
1273
|
+
(0, terminal_title_1.setTitle)(`maestro · running · ${device}`);
|
|
1274
|
+
}
|
|
1111
1275
|
// Log current status of runs (unless quiet mode)
|
|
1112
1276
|
if (!this.options.quiet) {
|
|
1113
1277
|
// Check if any run has flows and display them
|
|
@@ -1129,15 +1293,23 @@ class Maestro extends base_provider_1.default {
|
|
|
1129
1293
|
// Check if any flow has failed (for showing error column)
|
|
1130
1294
|
const hasFailures = this.hasAnyFlowFailed(allFlows);
|
|
1131
1295
|
if (!flowsTableDisplayed) {
|
|
1296
|
+
// Flows have arrived — stop the run-level spinner so it doesn't
|
|
1297
|
+
// fight the flow table's cursor-based in-place updates.
|
|
1298
|
+
this.spinner.stop();
|
|
1132
1299
|
// First time showing flows - display header and initial state
|
|
1133
1300
|
console.log(); // Empty line before flows table
|
|
1134
1301
|
this.displayFlowsTableHeader(hasFailures);
|
|
1135
1302
|
displayedLineCount = this.displayFlowsWithLimit(allFlows, previousFlowStatus, hasFailures);
|
|
1136
1303
|
flowsTableDisplayed = true;
|
|
1304
|
+
this.latestFlows = allFlows;
|
|
1305
|
+
this.latestDisplayedLineCount = displayedLineCount;
|
|
1306
|
+
this.startFlowAnimation(previousFlowStatus);
|
|
1137
1307
|
}
|
|
1138
1308
|
else {
|
|
1139
1309
|
// Update flows in place
|
|
1140
1310
|
displayedLineCount = this.updateFlowsInPlace(allFlows, previousFlowStatus, displayedLineCount);
|
|
1311
|
+
this.latestFlows = allFlows;
|
|
1312
|
+
this.latestDisplayedLineCount = displayedLineCount;
|
|
1141
1313
|
}
|
|
1142
1314
|
}
|
|
1143
1315
|
else {
|
|
@@ -1146,6 +1318,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1146
1318
|
}
|
|
1147
1319
|
}
|
|
1148
1320
|
if (status.completed) {
|
|
1321
|
+
this.stopFlowAnimation();
|
|
1149
1322
|
// Display final flows table with error messages if there are failures
|
|
1150
1323
|
if (!this.options.quiet && flowsTableDisplayed) {
|
|
1151
1324
|
const allFlows = [];
|
|
@@ -1162,9 +1335,9 @@ class Maestro extends base_provider_1.default {
|
|
|
1162
1335
|
process.stdout.write(`\x1b[${linesToMove}A`);
|
|
1163
1336
|
// Clear header line, write new header, then clear separator line
|
|
1164
1337
|
process.stdout.write('\x1b[2K');
|
|
1165
|
-
console.log(picocolors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(
|
|
1338
|
+
console.log(picocolors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(10)} Flow Fail reason`));
|
|
1166
1339
|
process.stdout.write('\x1b[2K');
|
|
1167
|
-
console.log(picocolors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(
|
|
1340
|
+
console.log(picocolors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
|
|
1168
1341
|
// Redraw all flows with error messages
|
|
1169
1342
|
for (const flow of allFlows) {
|
|
1170
1343
|
// Clear the line before writing
|
|
@@ -1175,21 +1348,27 @@ class Maestro extends base_provider_1.default {
|
|
|
1175
1348
|
}
|
|
1176
1349
|
// Print final summary
|
|
1177
1350
|
if (!this.options.quiet) {
|
|
1351
|
+
this.spinner.stop();
|
|
1178
1352
|
console.log(); // Empty line before summary
|
|
1179
1353
|
for (const run of status.runs) {
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1354
|
+
const passed = run.success === 1;
|
|
1355
|
+
const symbol = passed ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘');
|
|
1356
|
+
const statusText = passed
|
|
1357
|
+
? picocolors_1.default.green('Test completed successfully')
|
|
1358
|
+
: picocolors_1.default.red('Test failed');
|
|
1359
|
+
console.log(` ${symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusText}`);
|
|
1183
1360
|
}
|
|
1184
1361
|
}
|
|
1185
1362
|
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
1186
1363
|
if (allSucceeded) {
|
|
1364
|
+
(0, terminal_title_1.setTitle)('maestro · ✔ passed');
|
|
1187
1365
|
if (!this.options.quiet) {
|
|
1188
1366
|
logger_1.default.info('All tests completed successfully!');
|
|
1189
1367
|
}
|
|
1190
1368
|
}
|
|
1191
1369
|
else {
|
|
1192
1370
|
const failedRuns = status.runs.filter((run) => run.success !== 1);
|
|
1371
|
+
(0, terminal_title_1.setTitle)(`maestro · ✘ ${failedRuns.length} failed`);
|
|
1193
1372
|
logger_1.default.error(`${failedRuns.length} test run(s) failed`);
|
|
1194
1373
|
}
|
|
1195
1374
|
if (this.options.report && this.options.reportOutputDir) {
|
|
@@ -1203,35 +1382,50 @@ class Maestro extends base_provider_1.default {
|
|
|
1203
1382
|
runs: status.runs,
|
|
1204
1383
|
};
|
|
1205
1384
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1385
|
+
// Checked after getStatus() so a run that completes during the final
|
|
1386
|
+
// sleep is returned as success on the next iteration instead of being
|
|
1387
|
+
// misreported as a timeout.
|
|
1388
|
+
if (Date.now() - startTime >= this.MAX_POLL_DURATION_MS) {
|
|
1389
|
+
throw new testingbot_error_1.default(`Test timed out after ${this.MAX_POLL_DURATION_MS / 1000 / 60} minutes`);
|
|
1390
|
+
}
|
|
1391
|
+
const signature = JSON.stringify(status.runs.map((r) => [
|
|
1392
|
+
r.id,
|
|
1393
|
+
r.status,
|
|
1394
|
+
r.success,
|
|
1395
|
+
r.flows?.map((f) => [f.id, f.status]) ?? [],
|
|
1396
|
+
]));
|
|
1397
|
+
const changed = signature !== previousSignature;
|
|
1398
|
+
previousSignature = signature;
|
|
1399
|
+
pollInterval = this.computeNextPollInterval(pollInterval, changed);
|
|
1400
|
+
await this.sleep(pollInterval);
|
|
1208
1401
|
}
|
|
1209
|
-
throw new testingbot_error_1.default(`Test timed out after ${(this.MAX_POLL_ATTEMPTS * this.POLL_INTERVAL_MS) / 1000 / 60} minutes`);
|
|
1210
1402
|
}
|
|
1211
1403
|
displayRunStatus(runs, startTime, previousStatus) {
|
|
1212
1404
|
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
1213
1405
|
const elapsedStr = this.formatElapsedTime(elapsedSeconds);
|
|
1406
|
+
const activeMessages = [];
|
|
1214
1407
|
for (const run of runs) {
|
|
1215
1408
|
const prevStatus = previousStatus.get(run.id);
|
|
1216
1409
|
const statusChanged = prevStatus !== run.status;
|
|
1217
|
-
// If status changed from WAITING/READY to something else, clear the updating line
|
|
1218
|
-
if (statusChanged &&
|
|
1219
|
-
prevStatus &&
|
|
1220
|
-
(prevStatus === 'WAITING' || prevStatus === 'READY')) {
|
|
1221
|
-
this.clearLine();
|
|
1222
|
-
}
|
|
1223
1410
|
previousStatus.set(run.id, run.status);
|
|
1224
1411
|
const statusInfo = this.getStatusInfo(run.status);
|
|
1225
1412
|
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1413
|
+
const label = run.status === 'WAITING'
|
|
1414
|
+
? picocolors_1.default.yellow(statusInfo.text)
|
|
1415
|
+
: picocolors_1.default.cyan(statusInfo.text);
|
|
1416
|
+
activeMessages.push(`${label} ${picocolors_1.default.dim(`• Run ${run.id} (${this.getRunDisplayName(run)}) • ${elapsedStr}`)}`);
|
|
1229
1417
|
}
|
|
1230
1418
|
else if (statusChanged) {
|
|
1231
|
-
|
|
1232
|
-
console.log(` ${statusInfo.
|
|
1419
|
+
this.spinner.clearLine();
|
|
1420
|
+
console.log(` ${statusInfo.symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusInfo.text}`);
|
|
1233
1421
|
}
|
|
1234
1422
|
}
|
|
1423
|
+
if (activeMessages.length > 0) {
|
|
1424
|
+
this.spinner.setMessage(activeMessages.join(picocolors_1.default.dim(' ┊ ')));
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
this.spinner.stop();
|
|
1428
|
+
}
|
|
1235
1429
|
}
|
|
1236
1430
|
/**
|
|
1237
1431
|
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
@@ -1243,32 +1437,39 @@ class Maestro extends base_provider_1.default {
|
|
|
1243
1437
|
getStatusInfo(status) {
|
|
1244
1438
|
switch (status) {
|
|
1245
1439
|
case 'WAITING':
|
|
1246
|
-
return {
|
|
1440
|
+
return { symbol: picocolors_1.default.yellow('◐'), text: 'Waiting for test to start' };
|
|
1247
1441
|
case 'READY':
|
|
1248
|
-
return {
|
|
1442
|
+
return { symbol: picocolors_1.default.cyan('◑'), text: 'Running test' };
|
|
1249
1443
|
case 'DONE':
|
|
1250
|
-
return {
|
|
1444
|
+
return { symbol: picocolors_1.default.green('✔'), text: 'Test has finished running' };
|
|
1251
1445
|
case 'FAILED':
|
|
1252
|
-
return {
|
|
1446
|
+
return { symbol: picocolors_1.default.red('✘'), text: 'Test failed' };
|
|
1253
1447
|
default:
|
|
1254
|
-
return {
|
|
1448
|
+
return { symbol: picocolors_1.default.dim('?'), text: status };
|
|
1255
1449
|
}
|
|
1256
1450
|
}
|
|
1257
1451
|
getFlowStatusDisplay(flow) {
|
|
1452
|
+
const frame = FLOW_SPINNER_FRAMES[this.flowAnimationFrame];
|
|
1258
1453
|
switch (flow.status) {
|
|
1259
1454
|
case 'WAITING':
|
|
1260
|
-
return {
|
|
1455
|
+
return {
|
|
1456
|
+
text: `${frame} WAITING`,
|
|
1457
|
+
colored: picocolors_1.default.yellow(`${frame} WAITING`),
|
|
1458
|
+
};
|
|
1261
1459
|
case 'READY':
|
|
1262
|
-
return {
|
|
1460
|
+
return {
|
|
1461
|
+
text: `${frame} RUNNING`,
|
|
1462
|
+
colored: picocolors_1.default.cyan(`${frame} RUNNING`),
|
|
1463
|
+
};
|
|
1263
1464
|
case 'DONE':
|
|
1264
1465
|
if (flow.success === 1) {
|
|
1265
|
-
return { text: 'PASSED', colored: picocolors_1.default.green('PASSED') };
|
|
1466
|
+
return { text: '✔ PASSED', colored: picocolors_1.default.green('✔ PASSED') };
|
|
1266
1467
|
}
|
|
1267
1468
|
else {
|
|
1268
|
-
return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
|
|
1469
|
+
return { text: '✘ FAILED', colored: picocolors_1.default.red('✘ FAILED') };
|
|
1269
1470
|
}
|
|
1270
1471
|
case 'FAILED':
|
|
1271
|
-
return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
|
|
1472
|
+
return { text: '✘ FAILED', colored: picocolors_1.default.red('✘ FAILED') };
|
|
1272
1473
|
default:
|
|
1273
1474
|
return { text: flow.status, colored: flow.status };
|
|
1274
1475
|
}
|
|
@@ -1362,8 +1563,8 @@ class Maestro extends base_provider_1.default {
|
|
|
1362
1563
|
return linesWritten;
|
|
1363
1564
|
}
|
|
1364
1565
|
displayFlowsTableHeader(hasFailures = false) {
|
|
1365
|
-
let header = ` ${'Duration'.padEnd(10)} ${'Status'.padEnd(
|
|
1366
|
-
let separator = ` ${'─'.repeat(10)} ${'─'.repeat(
|
|
1566
|
+
let header = ` ${'Duration'.padEnd(10)} ${'Status'.padEnd(10)} Flow`;
|
|
1567
|
+
let separator = ` ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(30)}`;
|
|
1367
1568
|
if (hasFailures) {
|
|
1368
1569
|
header += ' Fail reason';
|
|
1369
1570
|
separator += ` ${'─'.repeat(80)}`;
|
|
@@ -1376,7 +1577,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1376
1577
|
const statusDisplay = this.getFlowStatusDisplay(flow);
|
|
1377
1578
|
// Pad based on display text length, add extra for color codes
|
|
1378
1579
|
const statusPadded = statusDisplay.colored +
|
|
1379
|
-
' '.repeat(Math.max(0,
|
|
1580
|
+
' '.repeat(Math.max(0, 10 - statusDisplay.text.length));
|
|
1380
1581
|
const name = flow.name.padEnd(30);
|
|
1381
1582
|
let linesWritten = 0;
|
|
1382
1583
|
const isFailed = flow.status === 'DONE' && flow.success !== 1;
|
|
@@ -1396,8 +1597,8 @@ class Maestro extends base_provider_1.default {
|
|
|
1396
1597
|
linesWritten++;
|
|
1397
1598
|
// Display remaining error messages on continuation lines
|
|
1398
1599
|
if (!isUpdate && hasFailures && isFailed && errorMessages.length > 1) {
|
|
1399
|
-
// Indent to align with the Fail reason column: Duration(11) + Status(
|
|
1400
|
-
const indent = ' '.repeat(
|
|
1600
|
+
// Indent to align with the Fail reason column: Duration(11) + Status(11) + Test(31) = 53 chars
|
|
1601
|
+
const indent = ' '.repeat(53);
|
|
1401
1602
|
for (let i = 1; i < errorMessages.length; i++) {
|
|
1402
1603
|
console.log(`${indent} ${picocolors_1.default.red(errorMessages[i])}`);
|
|
1403
1604
|
linesWritten++;
|
|
@@ -1420,6 +1621,33 @@ class Maestro extends base_provider_1.default {
|
|
|
1420
1621
|
}
|
|
1421
1622
|
return linesWritten;
|
|
1422
1623
|
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Starts the flow-table animation loop. Re-renders the cached flow rows at
|
|
1626
|
+
* `FLOW_ANIMATION_MS` so WAITING/RUNNING spinner frames advance between
|
|
1627
|
+
* (much slower) polls. Calling while already running is a no-op.
|
|
1628
|
+
*/
|
|
1629
|
+
startFlowAnimation(previousFlowStatus) {
|
|
1630
|
+
if (this.flowAnimationTimer || !utils_1.default.isInteractive())
|
|
1631
|
+
return;
|
|
1632
|
+
this.flowAnimationTimer = setInterval(() => {
|
|
1633
|
+
const hasActive = this.latestFlows.some((f) => f.status === 'WAITING' || f.status === 'READY');
|
|
1634
|
+
if (!hasActive)
|
|
1635
|
+
return;
|
|
1636
|
+
this.flowAnimationFrame =
|
|
1637
|
+
(this.flowAnimationFrame + 1) % FLOW_SPINNER_FRAMES.length;
|
|
1638
|
+
this.latestDisplayedLineCount = this.updateFlowsInPlace(this.latestFlows, previousFlowStatus, this.latestDisplayedLineCount);
|
|
1639
|
+
}, FLOW_ANIMATION_MS);
|
|
1640
|
+
this.flowAnimationTimer.unref?.();
|
|
1641
|
+
}
|
|
1642
|
+
stopFlowAnimation() {
|
|
1643
|
+
if (this.flowAnimationTimer) {
|
|
1644
|
+
clearInterval(this.flowAnimationTimer);
|
|
1645
|
+
this.flowAnimationTimer = null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
stopAnimations() {
|
|
1649
|
+
this.stopFlowAnimation();
|
|
1650
|
+
}
|
|
1423
1651
|
updateFlowsInPlace(flows, previousFlowStatus, displayedLineCount) {
|
|
1424
1652
|
const maxFlows = this.getMaxDisplayableFlows();
|
|
1425
1653
|
const displayFlows = flows.slice(0, maxFlows);
|
|
@@ -1434,7 +1662,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1434
1662
|
const duration = this.calculateFlowDuration(flow).padEnd(10);
|
|
1435
1663
|
const statusDisplay = this.getFlowStatusDisplay(flow);
|
|
1436
1664
|
const statusPadded = statusDisplay.colored +
|
|
1437
|
-
' '.repeat(Math.max(0,
|
|
1665
|
+
' '.repeat(Math.max(0, 10 - statusDisplay.text.length));
|
|
1438
1666
|
const name = flow.name;
|
|
1439
1667
|
const row = ` ${duration} ${statusPadded} ${name}`;
|
|
1440
1668
|
process.stdout.write(`\r\x1b[K${row}\n`);
|
|
@@ -1461,7 +1689,23 @@ class Maestro extends base_provider_1.default {
|
|
|
1461
1689
|
}
|
|
1462
1690
|
for (const run of runs) {
|
|
1463
1691
|
try {
|
|
1464
|
-
|
|
1692
|
+
let reportEndpoint;
|
|
1693
|
+
let reportKey;
|
|
1694
|
+
switch (reportFormat) {
|
|
1695
|
+
case 'junit':
|
|
1696
|
+
reportEndpoint = 'junit_report';
|
|
1697
|
+
reportKey = 'junit_report';
|
|
1698
|
+
break;
|
|
1699
|
+
case 'html-detailed':
|
|
1700
|
+
reportEndpoint = 'html_report_detailed';
|
|
1701
|
+
reportKey = 'html_report_detailed';
|
|
1702
|
+
break;
|
|
1703
|
+
case 'html':
|
|
1704
|
+
default:
|
|
1705
|
+
reportEndpoint = 'html_report';
|
|
1706
|
+
reportKey = 'html_report';
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1465
1709
|
const response = await axios_1.default.get(`${this.URL}/${this.appId}/${run.id}/${reportEndpoint}`, {
|
|
1466
1710
|
headers: {
|
|
1467
1711
|
'User-Agent': utils_1.default.getUserAgent(),
|
|
@@ -1470,13 +1714,12 @@ class Maestro extends base_provider_1.default {
|
|
|
1470
1714
|
username: this.credentials.userName,
|
|
1471
1715
|
password: this.credentials.accessKey,
|
|
1472
1716
|
},
|
|
1473
|
-
timeout:
|
|
1717
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
1474
1718
|
});
|
|
1475
1719
|
// Check for version update notification
|
|
1476
1720
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
1477
1721
|
utils_1.default.checkForUpdate(latestVersion);
|
|
1478
1722
|
// Extract the report content from the JSON response
|
|
1479
|
-
const reportKey = reportFormat === 'junit' ? 'junit_report' : 'html_report';
|
|
1480
1723
|
const reportContent = response.data[reportKey];
|
|
1481
1724
|
if (!reportContent) {
|
|
1482
1725
|
logger_1.default.error(`No ${reportFormat} report found for run ${run.id}`);
|
|
@@ -1506,7 +1749,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1506
1749
|
username: this.credentials.userName,
|
|
1507
1750
|
password: this.credentials.accessKey,
|
|
1508
1751
|
},
|
|
1509
|
-
timeout:
|
|
1752
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
1510
1753
|
});
|
|
1511
1754
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
1512
1755
|
utils_1.default.checkForUpdate(latestVersion);
|
|
@@ -1526,7 +1769,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1526
1769
|
return details;
|
|
1527
1770
|
}
|
|
1528
1771
|
attempts++;
|
|
1529
|
-
await this.sleep(this.
|
|
1772
|
+
await this.sleep(this.MIN_POLL_INTERVAL_MS);
|
|
1530
1773
|
}
|
|
1531
1774
|
throw new testingbot_error_1.default(`Timed out waiting for artifacts to sync for run ${runId}`);
|
|
1532
1775
|
}
|
|
@@ -1740,6 +1983,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1740
1983
|
const output = node_fs_1.default.createWriteStream(zipPath);
|
|
1741
1984
|
const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
|
|
1742
1985
|
output.on('close', () => resolve());
|
|
1986
|
+
output.on('error', (err) => reject(err));
|
|
1743
1987
|
archive.on('error', (err) => reject(err));
|
|
1744
1988
|
archive.pipe(output);
|
|
1745
1989
|
archive.directory(sourceDir, false);
|
|
@@ -1754,9 +1998,9 @@ class Maestro extends base_provider_1.default {
|
|
|
1754
1998
|
this.socket = (0, socket_io_client_1.io)(this.updateServer, {
|
|
1755
1999
|
transports: ['websocket'],
|
|
1756
2000
|
reconnection: true,
|
|
1757
|
-
reconnectionAttempts:
|
|
1758
|
-
reconnectionDelay:
|
|
1759
|
-
timeout:
|
|
2001
|
+
reconnectionAttempts: constants_1.SOCKET.RECONNECTION_ATTEMPTS,
|
|
2002
|
+
reconnectionDelay: constants_1.SOCKET.RECONNECTION_DELAY_MS,
|
|
2003
|
+
timeout: constants_1.SOCKET.TIMEOUT_MS,
|
|
1760
2004
|
});
|
|
1761
2005
|
this.socket.on('connect', () => {
|
|
1762
2006
|
// Join the room for this test run
|
|
@@ -1768,8 +2012,12 @@ class Maestro extends base_provider_1.default {
|
|
|
1768
2012
|
this.socket.on('maestro_error', (data) => {
|
|
1769
2013
|
this.handleMaestroError(data);
|
|
1770
2014
|
});
|
|
1771
|
-
this.socket.on('connect_error', () => {
|
|
1772
|
-
|
|
2015
|
+
this.socket.on('connect_error', (err) => {
|
|
2016
|
+
if (!this.socketFallbackWarned) {
|
|
2017
|
+
this.socketFallbackWarned = true;
|
|
2018
|
+
logger_1.default.warn('Real-time log stream unavailable, falling back to polling.');
|
|
2019
|
+
logger_1.default.debug(`Socket connect_error: ${err?.message ?? 'unknown error'}`);
|
|
2020
|
+
}
|
|
1773
2021
|
this.disconnectFromUpdateServer();
|
|
1774
2022
|
});
|
|
1775
2023
|
}
|
|
@@ -1788,8 +2036,8 @@ class Maestro extends base_provider_1.default {
|
|
|
1788
2036
|
try {
|
|
1789
2037
|
const message = JSON.parse(data);
|
|
1790
2038
|
if (message.payload) {
|
|
1791
|
-
// Clear the
|
|
1792
|
-
this.clearLine();
|
|
2039
|
+
// Clear the spinner line before printing output
|
|
2040
|
+
this.spinner.clearLine();
|
|
1793
2041
|
// Print the Maestro output, trimming trailing newlines
|
|
1794
2042
|
process.stdout.write(message.payload);
|
|
1795
2043
|
}
|
|
@@ -1802,8 +2050,8 @@ class Maestro extends base_provider_1.default {
|
|
|
1802
2050
|
try {
|
|
1803
2051
|
const message = JSON.parse(data);
|
|
1804
2052
|
if (message.payload) {
|
|
1805
|
-
// Clear the
|
|
1806
|
-
this.clearLine();
|
|
2053
|
+
// Clear the spinner line before printing error
|
|
2054
|
+
this.spinner.clearLine();
|
|
1807
2055
|
// Print the error output
|
|
1808
2056
|
process.stderr.write(message.payload);
|
|
1809
2057
|
}
|