@vizzly-testing/cli 0.34.0 → 0.35.1
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 +67 -7
- package/dist/api/core.js +18 -4
- package/dist/api/endpoints.js +38 -15
- package/dist/api/index.js +1 -1
- package/dist/cli.js +270 -194
- package/dist/client/index.js +62 -21
- package/dist/commands/api.js +9 -3
- package/dist/commands/preview.js +30 -26
- package/dist/commands/project.js +1 -1
- package/dist/commands/run.js +25 -10
- package/dist/commands/status.js +14 -1
- package/dist/commands/tdd-daemon-process.js +5 -1
- package/dist/commands/tdd-daemon.js +217 -139
- package/dist/commands/tdd.js +26 -5
- package/dist/commands/upload.js +31 -13
- package/dist/reporter/reporter-bundle.iife.js +6 -6
- package/dist/sdk/index.js +23 -9
- package/dist/server/handlers/api-handler.js +3 -2
- package/dist/server/handlers/tdd-handler.js +5 -1
- package/dist/server/routers/screenshot.js +11 -4
- package/dist/services/build-manager.js +6 -1
- package/dist/tdd/tdd-service.js +6 -2
- package/dist/test-runner/core.js +8 -3
- package/dist/test-runner/operations.js +19 -3
- package/dist/types/client.d.ts +32 -5
- package/dist/types/index.d.ts +33 -5
- package/dist/uploader/core.js +41 -9
- package/dist/uploader/operations.js +19 -17
- package/dist/utils/config-loader.js +13 -6
- package/dist/utils/context.js +46 -76
- package/dist/utils/environment-config.js +20 -0
- package/dist/utils/output.js +8 -5
- package/dist/utils/screenshot-options.js +60 -10
- package/package.json +1 -1
- package/skills/vizzly/SKILL.md +12 -4
- package/skills/vizzly/references/cli-context.md +8 -2
- package/skills/vizzly/references/setup-ci.md +3 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { basename, join } from 'node:path';
|
|
5
4
|
import { getServerRegistry } from '../tdd/server-registry.js';
|
|
6
5
|
import { withTimeout } from '../utils/async-utils.js';
|
|
7
6
|
import * as output from '../utils/output.js';
|
|
8
7
|
import { tddCommand } from './tdd.js';
|
|
9
|
-
import { resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
|
|
8
|
+
import { parsePositiveInteger, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
|
|
10
9
|
export { findDaemonPidByPort, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
|
|
11
10
|
let defaultTimers = {
|
|
12
11
|
setTimeout,
|
|
@@ -40,55 +39,30 @@ export function cleanupLocalDaemonFiles(directory = process.cwd(), deps = {}) {
|
|
|
40
39
|
serverFileRemoved: removeFileIfExists(serverFile, deps)
|
|
41
40
|
};
|
|
42
41
|
}
|
|
43
|
-
export function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
mkdir(globalVizzlyDir, {
|
|
67
|
-
recursive: true
|
|
68
|
-
});
|
|
42
|
+
export function readLocalDaemonInfo(directory = process.cwd(), deps = {}) {
|
|
43
|
+
let {
|
|
44
|
+
serverFile
|
|
45
|
+
} = getLocalDaemonFiles(directory);
|
|
46
|
+
let fileExists = deps.existsSync || existsSync;
|
|
47
|
+
let readFile = deps.readFileSync || readFileSync;
|
|
48
|
+
if (!fileExists(serverFile)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
let info = JSON.parse(readFile(serverFile, 'utf8'));
|
|
53
|
+
let port = parsePositiveInteger(info.port);
|
|
54
|
+
let pid = parsePositiveInteger(info.pid);
|
|
55
|
+
if (!port) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...info,
|
|
60
|
+
port,
|
|
61
|
+
pid
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
69
65
|
}
|
|
70
|
-
let globalServerFile = join(globalVizzlyDir, 'server.json');
|
|
71
|
-
let serverInfo = buildLegacyServerInfo({
|
|
72
|
-
pid,
|
|
73
|
-
port,
|
|
74
|
-
now
|
|
75
|
-
});
|
|
76
|
-
writeFile(globalServerFile, JSON.stringify(serverInfo, null, 2));
|
|
77
|
-
return {
|
|
78
|
-
path: globalServerFile,
|
|
79
|
-
serverInfo
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
export function cleanupLegacyGlobalServerFile({
|
|
83
|
-
home = homedir,
|
|
84
|
-
exists = existsSync,
|
|
85
|
-
unlink = unlinkSync
|
|
86
|
-
} = {}) {
|
|
87
|
-
let globalServerFile = join(home(), '.vizzly', 'server.json');
|
|
88
|
-
return removeFileIfExists(globalServerFile, {
|
|
89
|
-
existsSync: exists,
|
|
90
|
-
unlinkSync: unlink
|
|
91
|
-
});
|
|
92
66
|
}
|
|
93
67
|
export function unregisterDaemonServer({
|
|
94
68
|
port,
|
|
@@ -105,10 +79,12 @@ export function cleanupDaemonState({
|
|
|
105
79
|
directory = process.cwd(),
|
|
106
80
|
registry = getServerRegistry(),
|
|
107
81
|
localFileDeps = {},
|
|
108
|
-
|
|
82
|
+
cleanLocalFiles = true
|
|
109
83
|
} = {}) {
|
|
110
|
-
let
|
|
111
|
-
|
|
84
|
+
let cleanupResult = cleanLocalFiles ? cleanupLocalDaemonFiles(directory, localFileDeps) : {
|
|
85
|
+
pidFileRemoved: false,
|
|
86
|
+
serverFileRemoved: false
|
|
87
|
+
};
|
|
112
88
|
try {
|
|
113
89
|
if (port !== undefined) {
|
|
114
90
|
unregisterDaemonServer({
|
|
@@ -124,11 +100,42 @@ export function cleanupDaemonState({
|
|
|
124
100
|
} catch {
|
|
125
101
|
// Non-fatal; stale file cleanup is still useful on its own.
|
|
126
102
|
}
|
|
103
|
+
return cleanupResult;
|
|
104
|
+
}
|
|
105
|
+
function resolveDaemonTarget(options = {}, directory = process.cwd()) {
|
|
106
|
+
let registry = getServerRegistry();
|
|
107
|
+
let requestedPort = options.port ? Number(options.port) : null;
|
|
108
|
+
let localInfo = readLocalDaemonInfo(directory);
|
|
109
|
+
let localInfoMatches = !requestedPort || localInfo?.port === requestedPort ? localInfo : null;
|
|
110
|
+
let registryServer = requestedPort ? registry.find({
|
|
111
|
+
port: requestedPort
|
|
112
|
+
}) : registry.find({
|
|
113
|
+
directory
|
|
114
|
+
});
|
|
115
|
+
let serverInfo = localInfoMatches || registryServer || (requestedPort ? {
|
|
116
|
+
port: requestedPort
|
|
117
|
+
} : null);
|
|
118
|
+
let port = requestedPort || serverInfo?.port || 47392;
|
|
127
119
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
registry,
|
|
121
|
+
requestedPort,
|
|
122
|
+
localInfo: localInfoMatches,
|
|
123
|
+
registryServer,
|
|
124
|
+
serverInfo,
|
|
125
|
+
port,
|
|
126
|
+
directory: registryServer?.directory || directory,
|
|
127
|
+
useLocalPidFile: !requestedPort || Boolean(localInfoMatches),
|
|
128
|
+
cleanLocalFiles: !requestedPort || Boolean(localInfoMatches || registryServer)
|
|
130
129
|
};
|
|
131
130
|
}
|
|
131
|
+
function cleanupDaemonTarget(target) {
|
|
132
|
+
return cleanupDaemonState({
|
|
133
|
+
port: target.port,
|
|
134
|
+
directory: target.directory,
|
|
135
|
+
registry: target.registry,
|
|
136
|
+
cleanLocalFiles: target.cleanLocalFiles
|
|
137
|
+
});
|
|
138
|
+
}
|
|
132
139
|
export function buildDashboardUrl(port = 47392) {
|
|
133
140
|
return `http://localhost:${port}`;
|
|
134
141
|
}
|
|
@@ -196,29 +203,62 @@ export async function waitForDaemonChildInit(child, {
|
|
|
196
203
|
timers = defaultTimers
|
|
197
204
|
} = {}) {
|
|
198
205
|
let cleanup = () => {};
|
|
206
|
+
let stderrOutput = '';
|
|
199
207
|
let initPromise = new Promise(resolve => {
|
|
208
|
+
let handleStderr = data => {
|
|
209
|
+
stderrOutput += data.toString();
|
|
210
|
+
};
|
|
211
|
+
let handleMessage = message => {
|
|
212
|
+
if (message?.type !== 'error') {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let error = new Error(message.message || 'Daemon child failed to start');
|
|
216
|
+
if (message.code) {
|
|
217
|
+
error.code = message.code;
|
|
218
|
+
}
|
|
219
|
+
if (message.stack) {
|
|
220
|
+
error.stack = message.stack;
|
|
221
|
+
}
|
|
222
|
+
cleanup();
|
|
223
|
+
resolve({
|
|
224
|
+
ok: false,
|
|
225
|
+
reason: 'error',
|
|
226
|
+
error
|
|
227
|
+
});
|
|
228
|
+
};
|
|
200
229
|
let handleDisconnect = () => {
|
|
201
230
|
cleanup();
|
|
202
231
|
resolve({
|
|
203
232
|
ok: true
|
|
204
233
|
});
|
|
205
234
|
};
|
|
206
|
-
let handleExit = () => {
|
|
235
|
+
let handleExit = (code, signal) => {
|
|
236
|
+
let error = new Error(stderrOutput.trim() || 'Daemon child exited before startup');
|
|
237
|
+
error.code = code ?? signal ?? null;
|
|
207
238
|
cleanup();
|
|
208
239
|
resolve({
|
|
209
240
|
ok: false,
|
|
210
|
-
reason: 'exit'
|
|
241
|
+
reason: 'exit',
|
|
242
|
+
error
|
|
211
243
|
});
|
|
212
244
|
};
|
|
213
245
|
cleanup = () => {
|
|
246
|
+
child.stderr?.off?.('data', handleStderr);
|
|
247
|
+
child.off('message', handleMessage);
|
|
214
248
|
child.off('disconnect', handleDisconnect);
|
|
215
249
|
child.off('exit', handleExit);
|
|
216
250
|
};
|
|
251
|
+
child.stderr?.on?.('data', handleStderr);
|
|
252
|
+
child.on('message', handleMessage);
|
|
217
253
|
child.on('disconnect', handleDisconnect);
|
|
218
254
|
child.on('exit', handleExit);
|
|
219
255
|
});
|
|
220
256
|
try {
|
|
221
|
-
|
|
257
|
+
let result = await withTimeout(initPromise, timeoutMs, 'TDD server initialization timed out', timers);
|
|
258
|
+
if (result.ok) {
|
|
259
|
+
child.stderr?.unref?.();
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
222
262
|
} catch (error) {
|
|
223
263
|
cleanup();
|
|
224
264
|
return {
|
|
@@ -301,7 +341,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
301
341
|
let autoAllocated = false;
|
|
302
342
|
if (options.port) {
|
|
303
343
|
// User specified a port - use it (will fail if busy)
|
|
304
|
-
port = options.port;
|
|
344
|
+
port = Number(options.port);
|
|
305
345
|
|
|
306
346
|
// Check if user-specified port is in use
|
|
307
347
|
if (await isServerRunning(port)) {
|
|
@@ -339,14 +379,15 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
339
379
|
output.startSpinner(`Downloading baselines from build ${options.baselineBuild}...`);
|
|
340
380
|
}
|
|
341
381
|
|
|
342
|
-
//
|
|
382
|
+
// Keep daemon stdio detached so callers that capture output do not hang
|
|
383
|
+
// waiting for the long-lived child to close inherited pipes.
|
|
343
384
|
let child = spawn(process.execPath, buildDaemonChildArgs({
|
|
344
385
|
port,
|
|
345
386
|
options,
|
|
346
387
|
globalOptions
|
|
347
388
|
}), {
|
|
348
389
|
detached: true,
|
|
349
|
-
stdio: ['ignore', '
|
|
390
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
350
391
|
cwd: process.cwd()
|
|
351
392
|
});
|
|
352
393
|
let initResult = await waitForDaemonChildInit(child);
|
|
@@ -354,7 +395,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
354
395
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
355
396
|
output.stopSpinner();
|
|
356
397
|
}
|
|
357
|
-
output.error('TDD server failed to start');
|
|
398
|
+
output.error(initResult.error?.message || 'TDD server failed to start');
|
|
358
399
|
process.exit(1);
|
|
359
400
|
}
|
|
360
401
|
|
|
@@ -394,16 +435,6 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
394
435
|
// Non-fatal
|
|
395
436
|
}
|
|
396
437
|
|
|
397
|
-
// Also write legacy server.json for SDK discovery (backwards compatibility)
|
|
398
|
-
try {
|
|
399
|
-
writeLegacyGlobalServerFile({
|
|
400
|
-
pid: child.pid,
|
|
401
|
-
port
|
|
402
|
-
});
|
|
403
|
-
} catch {
|
|
404
|
-
// Non-fatal, SDK can still use health check
|
|
405
|
-
}
|
|
406
|
-
|
|
407
438
|
// JSON output for successful start
|
|
408
439
|
let dashboardUrl = buildDashboardUrl(port);
|
|
409
440
|
if (globalOptions.json) {
|
|
@@ -442,7 +473,8 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
442
473
|
|
|
443
474
|
// Always show stop hint
|
|
444
475
|
output.blank();
|
|
445
|
-
|
|
476
|
+
let stopCommand = port === 47392 ? 'vizzly tdd stop' : `vizzly tdd stop --port ${port}`;
|
|
477
|
+
output.hint(`Stop with: ${stopCommand}`);
|
|
446
478
|
if (options.open) {
|
|
447
479
|
openDashboard(port);
|
|
448
480
|
}
|
|
@@ -475,6 +507,7 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
475
507
|
try {
|
|
476
508
|
// Use existing tddCommand but with daemon mode
|
|
477
509
|
let {
|
|
510
|
+
result,
|
|
478
511
|
cleanup
|
|
479
512
|
} = await tddCommand(null,
|
|
480
513
|
// No test command - server only
|
|
@@ -482,6 +515,9 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
482
515
|
...options,
|
|
483
516
|
daemon: true
|
|
484
517
|
}, globalOptions);
|
|
518
|
+
if (result?.success === false) {
|
|
519
|
+
throw new Error(result.error || 'TDD server failed to start');
|
|
520
|
+
}
|
|
485
521
|
|
|
486
522
|
// Disconnect IPC after successful initialization to signal parent
|
|
487
523
|
if (process.send) {
|
|
@@ -521,11 +557,29 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
521
557
|
// Keep process alive
|
|
522
558
|
process.stdin.resume();
|
|
523
559
|
} catch (error) {
|
|
524
|
-
|
|
560
|
+
await sendDaemonChildError(error);
|
|
525
561
|
console.error(`Fatal error: ${error.message}`);
|
|
526
562
|
process.exit(1);
|
|
527
563
|
}
|
|
528
564
|
}
|
|
565
|
+
async function sendDaemonChildError(error) {
|
|
566
|
+
if (!process.send) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
let message = {
|
|
570
|
+
type: 'error',
|
|
571
|
+
message: error.message,
|
|
572
|
+
code: error.code || null,
|
|
573
|
+
stack: error.stack || null
|
|
574
|
+
};
|
|
575
|
+
await new Promise(resolve => {
|
|
576
|
+
try {
|
|
577
|
+
process.send(message, () => resolve());
|
|
578
|
+
} catch {
|
|
579
|
+
resolve();
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
529
583
|
|
|
530
584
|
/**
|
|
531
585
|
* Stop TDD daemon server
|
|
@@ -538,10 +592,16 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
538
592
|
verbose: globalOptions.verbose,
|
|
539
593
|
color: !globalOptions.noColor
|
|
540
594
|
});
|
|
541
|
-
let
|
|
542
|
-
let pid = await resolveDaemonPid({
|
|
543
|
-
port
|
|
544
|
-
});
|
|
595
|
+
let target = resolveDaemonTarget(options);
|
|
596
|
+
let pid = target.useLocalPidFile ? await resolveDaemonPid({
|
|
597
|
+
port: target.port
|
|
598
|
+
}) : null;
|
|
599
|
+
|
|
600
|
+
// Stop is intentionally file/registry-driven. Do not rediscover arbitrary
|
|
601
|
+
// processes by port here; a non-Vizzly server may be using that port.
|
|
602
|
+
if (!pid) {
|
|
603
|
+
pid = target.serverInfo?.pid || null;
|
|
604
|
+
}
|
|
545
605
|
if (!pid) {
|
|
546
606
|
// JSON output for not running
|
|
547
607
|
if (globalOptions.json) {
|
|
@@ -554,9 +614,7 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
554
614
|
}
|
|
555
615
|
|
|
556
616
|
// Clean up any stale files
|
|
557
|
-
|
|
558
|
-
port
|
|
559
|
-
});
|
|
617
|
+
cleanupDaemonTarget(target);
|
|
560
618
|
return;
|
|
561
619
|
}
|
|
562
620
|
try {
|
|
@@ -576,16 +634,14 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
576
634
|
}
|
|
577
635
|
|
|
578
636
|
// Clean up files
|
|
579
|
-
|
|
580
|
-
port
|
|
581
|
-
});
|
|
637
|
+
cleanupDaemonTarget(target);
|
|
582
638
|
|
|
583
639
|
// JSON output for successful stop
|
|
584
640
|
if (globalOptions.json) {
|
|
585
641
|
output.data({
|
|
586
642
|
status: 'stopped',
|
|
587
643
|
pid,
|
|
588
|
-
port
|
|
644
|
+
port: target.port
|
|
589
645
|
});
|
|
590
646
|
return;
|
|
591
647
|
}
|
|
@@ -593,10 +649,16 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
593
649
|
} catch (error) {
|
|
594
650
|
if (error.code === 'ESRCH') {
|
|
595
651
|
// Process not found - clean up stale files
|
|
652
|
+
cleanupDaemonTarget(target);
|
|
653
|
+
if (globalOptions.json) {
|
|
654
|
+
output.data({
|
|
655
|
+
status: 'stale',
|
|
656
|
+
stopped: false,
|
|
657
|
+
message: 'TDD server was not running; cleaned up stale files'
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
596
661
|
output.warn('TDD server was not running (cleaning up stale files)');
|
|
597
|
-
cleanupDaemonState({
|
|
598
|
-
port
|
|
599
|
-
});
|
|
600
662
|
} else {
|
|
601
663
|
output.error('Error stopping TDD server', error);
|
|
602
664
|
}
|
|
@@ -608,17 +670,53 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
608
670
|
* @param {Object} options - Command options
|
|
609
671
|
* @param {Object} globalOptions - Global CLI options
|
|
610
672
|
*/
|
|
611
|
-
|
|
673
|
+
function printRunningStatus({
|
|
674
|
+
serverInfo,
|
|
675
|
+
port,
|
|
676
|
+
pid,
|
|
677
|
+
uptimeMs = null,
|
|
678
|
+
uptimeStr = '',
|
|
679
|
+
dashboardUrl = buildDashboardUrl(port),
|
|
680
|
+
globalOptions = {}
|
|
681
|
+
}) {
|
|
682
|
+
if (globalOptions.json) {
|
|
683
|
+
output.data({
|
|
684
|
+
running: true,
|
|
685
|
+
port,
|
|
686
|
+
pid,
|
|
687
|
+
uptimeMs,
|
|
688
|
+
uptime: uptimeStr || null,
|
|
689
|
+
dashboardUrl
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
let colors = output.getColors();
|
|
694
|
+
output.header('tdd', 'local');
|
|
695
|
+
output.print(` ${output.statusDot('success')} Running ${uptimeStr ? colors.brand.textTertiary(`· ${uptimeStr}`) : ''}`);
|
|
696
|
+
output.blank();
|
|
697
|
+
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
698
|
+
title: 'Dashboard',
|
|
699
|
+
style: 'branded'
|
|
700
|
+
});
|
|
701
|
+
if (globalOptions.verbose && pid) {
|
|
702
|
+
output.blank();
|
|
703
|
+
output.print(` ${colors.brand.textTertiary('PID:')} ${pid}`);
|
|
704
|
+
}
|
|
705
|
+
if (globalOptions.verbose && serverInfo?.logFile) {
|
|
706
|
+
output.print(` ${colors.brand.textTertiary('Log:')} ${serverInfo.logFile}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
export async function tddStatusCommand(options = {}, globalOptions = {}) {
|
|
612
710
|
output.configure({
|
|
613
711
|
json: globalOptions.json,
|
|
614
712
|
verbose: globalOptions.verbose,
|
|
615
713
|
color: !globalOptions.noColor
|
|
616
714
|
});
|
|
617
715
|
let {
|
|
618
|
-
pidFile
|
|
619
|
-
serverFile
|
|
716
|
+
pidFile
|
|
620
717
|
} = getLocalDaemonFiles();
|
|
621
|
-
|
|
718
|
+
let target = resolveDaemonTarget(options);
|
|
719
|
+
if (!target.serverInfo && !existsSync(pidFile)) {
|
|
622
720
|
// JSON output for not running
|
|
623
721
|
if (globalOptions.json) {
|
|
624
722
|
output.data({
|
|
@@ -631,31 +729,27 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
631
729
|
return;
|
|
632
730
|
}
|
|
633
731
|
try {
|
|
634
|
-
let pid =
|
|
732
|
+
let pid = target.useLocalPidFile ? readDaemonPidFile(pidFile) : null;
|
|
733
|
+
if (!pid) {
|
|
734
|
+
pid = target.serverInfo?.pid || null;
|
|
735
|
+
}
|
|
635
736
|
if (!pid) {
|
|
636
|
-
output.warn('TDD server
|
|
637
|
-
|
|
737
|
+
output.warn('TDD server not running (cleaning up stale files)');
|
|
738
|
+
cleanupDaemonTarget(target);
|
|
638
739
|
return;
|
|
639
740
|
}
|
|
640
741
|
|
|
641
742
|
// Check if process is actually running
|
|
642
743
|
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
643
744
|
|
|
644
|
-
let serverInfo = {
|
|
645
|
-
port: 47392
|
|
646
|
-
};
|
|
647
|
-
if (existsSync(serverFile)) {
|
|
648
|
-
serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
745
|
// Try to check health endpoint
|
|
652
|
-
let health = await checkServerHealth(
|
|
746
|
+
let health = await checkServerHealth(target.port);
|
|
653
747
|
if (health.running) {
|
|
654
748
|
// Calculate uptime
|
|
655
749
|
let uptimeMs = null;
|
|
656
750
|
let uptimeStr = '';
|
|
657
|
-
if (serverInfo
|
|
658
|
-
uptimeMs = Date.now() - serverInfo.startTime;
|
|
751
|
+
if (target.serverInfo?.startTime) {
|
|
752
|
+
uptimeMs = Date.now() - target.serverInfo.startTime;
|
|
659
753
|
let uptime = Math.floor(uptimeMs / 1000);
|
|
660
754
|
let hours = Math.floor(uptime / 3600);
|
|
661
755
|
let minutes = Math.floor(uptime % 3600 / 60);
|
|
@@ -664,47 +758,31 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
664
758
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
665
759
|
uptimeStr += `${seconds}s`;
|
|
666
760
|
}
|
|
667
|
-
let dashboardUrl = buildDashboardUrl(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
uptime: uptimeStr || null,
|
|
677
|
-
dashboardUrl
|
|
678
|
-
});
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
let colors = output.getColors();
|
|
682
|
-
|
|
683
|
-
// Show header
|
|
684
|
-
output.header('tdd', 'local');
|
|
685
|
-
|
|
686
|
-
// Show running status with uptime
|
|
687
|
-
output.print(` ${output.statusDot('success')} Running ${uptimeStr ? colors.brand.textTertiary(`· ${uptimeStr}`) : ''}`);
|
|
688
|
-
output.blank();
|
|
689
|
-
|
|
690
|
-
// Show dashboard URL in a branded box
|
|
691
|
-
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
692
|
-
title: 'Dashboard',
|
|
693
|
-
style: 'branded'
|
|
761
|
+
let dashboardUrl = buildDashboardUrl(target.port);
|
|
762
|
+
printRunningStatus({
|
|
763
|
+
serverInfo: target.serverInfo,
|
|
764
|
+
port: target.port,
|
|
765
|
+
pid,
|
|
766
|
+
uptimeMs,
|
|
767
|
+
uptimeStr,
|
|
768
|
+
dashboardUrl,
|
|
769
|
+
globalOptions
|
|
694
770
|
});
|
|
695
|
-
|
|
696
|
-
// Verbose mode: show PID
|
|
697
|
-
if (globalOptions.verbose) {
|
|
698
|
-
output.blank();
|
|
699
|
-
output.print(` ${colors.brand.textTertiary('PID:')} ${pid}`);
|
|
700
|
-
}
|
|
701
771
|
} else {
|
|
702
772
|
output.warn('TDD server process exists but not responding to health checks');
|
|
703
773
|
}
|
|
704
774
|
} catch (error) {
|
|
705
775
|
if (error.code === 'ESRCH') {
|
|
776
|
+
cleanupDaemonTarget(target);
|
|
777
|
+
if (globalOptions.json) {
|
|
778
|
+
output.data({
|
|
779
|
+
status: 'stale',
|
|
780
|
+
running: false,
|
|
781
|
+
message: 'TDD server process not found; cleaned up stale files'
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
706
785
|
output.warn('TDD server process not found (cleaning up stale files)');
|
|
707
|
-
cleanupDaemonState();
|
|
708
786
|
} else {
|
|
709
787
|
output.error('Error checking TDD server status', error);
|
|
710
788
|
}
|
package/dist/commands/tdd.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
7
7
|
import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../api/index.js';
|
|
8
|
+
import { CONFIG_DEFAULTS } from '../config/core.js';
|
|
8
9
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
9
10
|
import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
|
|
10
11
|
import { createAuthService as defaultCreateAuthService } from '../services/auth-service.js';
|
|
@@ -13,7 +14,7 @@ import { createConfigService as defaultCreateConfigService } from '../services/c
|
|
|
13
14
|
import { createProjectService as defaultCreateProjectService } from '../services/project-service.js';
|
|
14
15
|
import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../test-runner/index.js';
|
|
15
16
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
16
|
-
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
|
|
17
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
17
18
|
import * as defaultOutput from '../utils/output.js';
|
|
18
19
|
function buildLocalContextCommand() {
|
|
19
20
|
return 'vizzly context build current --source local --agent';
|
|
@@ -43,6 +44,9 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
43
44
|
runTests = defaultRunTests,
|
|
44
45
|
detectBranch = defaultDetectBranch,
|
|
45
46
|
detectCommit = defaultDetectCommit,
|
|
47
|
+
detectCommitMessage = defaultDetectCommitMessage,
|
|
48
|
+
detectPullRequestNumber = defaultDetectPullRequestNumber,
|
|
49
|
+
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
|
|
46
50
|
spawn = defaultSpawn,
|
|
47
51
|
output = defaultOutput
|
|
48
52
|
} = deps;
|
|
@@ -76,7 +80,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
76
80
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
77
81
|
|
|
78
82
|
// Dev mode works locally by default - only needs token for baseline download
|
|
79
|
-
let needsToken = options.baselineBuild || options.baselineComparison;
|
|
83
|
+
let needsToken = options.baselineBuild || options.baselineComparison || config.baselineBuildId || config.baselineComparisonId;
|
|
80
84
|
if (!config.apiKey && needsToken) {
|
|
81
85
|
throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
|
|
82
86
|
}
|
|
@@ -85,8 +89,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
85
89
|
config.allowNoToken = true;
|
|
86
90
|
|
|
87
91
|
// Collect git metadata
|
|
88
|
-
let
|
|
89
|
-
let
|
|
92
|
+
let configuredBuildName = config.build.name && config.build.name !== CONFIG_DEFAULTS.build.name ? config.build.name : undefined;
|
|
93
|
+
let branch = await detectBranch(options.branch || config.build.branch);
|
|
94
|
+
let commit = await detectCommit(options.commit || config.build.commit);
|
|
95
|
+
let message = options.message || config.build.message || (await detectCommitMessage());
|
|
96
|
+
let buildName = await generateBuildNameWithGit(options.buildName || configuredBuildName);
|
|
97
|
+
let pullRequestNumber = detectPullRequestNumber();
|
|
90
98
|
|
|
91
99
|
// Show header (skip in daemon mode)
|
|
92
100
|
if (!options.daemon) {
|
|
@@ -132,15 +140,28 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
132
140
|
tdd: true,
|
|
133
141
|
daemon: options.daemon || false,
|
|
134
142
|
setBaseline: options.setBaseline || false,
|
|
143
|
+
failOnDiff: options.failOnDiff || false,
|
|
144
|
+
name: buildName,
|
|
145
|
+
buildName,
|
|
135
146
|
branch,
|
|
136
147
|
commit,
|
|
148
|
+
message,
|
|
137
149
|
environment: config.build.environment,
|
|
150
|
+
pullRequestNumber,
|
|
151
|
+
parallelId: config.parallelId,
|
|
152
|
+
metadata: {
|
|
153
|
+
comparison: {
|
|
154
|
+
threshold: config.comparison.threshold,
|
|
155
|
+
minClusterSize: config.comparison.minClusterSize
|
|
156
|
+
}
|
|
157
|
+
},
|
|
138
158
|
threshold: config.comparison.threshold,
|
|
139
159
|
minClusterSize: config.comparison.minClusterSize,
|
|
140
160
|
allowNoToken: config.allowNoToken || false,
|
|
141
161
|
baselineBuildId: config.baselineBuildId,
|
|
142
162
|
baselineComparisonId: config.baselineComparisonId,
|
|
143
|
-
wait: false
|
|
163
|
+
wait: false,
|
|
164
|
+
json: globalOptions.json
|
|
144
165
|
};
|
|
145
166
|
|
|
146
167
|
// In daemon mode, just start the server without running tests
|