@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.
Files changed (37) hide show
  1. package/README.md +67 -7
  2. package/dist/api/core.js +18 -4
  3. package/dist/api/endpoints.js +38 -15
  4. package/dist/api/index.js +1 -1
  5. package/dist/cli.js +270 -194
  6. package/dist/client/index.js +62 -21
  7. package/dist/commands/api.js +9 -3
  8. package/dist/commands/preview.js +30 -26
  9. package/dist/commands/project.js +1 -1
  10. package/dist/commands/run.js +25 -10
  11. package/dist/commands/status.js +14 -1
  12. package/dist/commands/tdd-daemon-process.js +5 -1
  13. package/dist/commands/tdd-daemon.js +217 -139
  14. package/dist/commands/tdd.js +26 -5
  15. package/dist/commands/upload.js +31 -13
  16. package/dist/reporter/reporter-bundle.iife.js +6 -6
  17. package/dist/sdk/index.js +23 -9
  18. package/dist/server/handlers/api-handler.js +3 -2
  19. package/dist/server/handlers/tdd-handler.js +5 -1
  20. package/dist/server/routers/screenshot.js +11 -4
  21. package/dist/services/build-manager.js +6 -1
  22. package/dist/tdd/tdd-service.js +6 -2
  23. package/dist/test-runner/core.js +8 -3
  24. package/dist/test-runner/operations.js +19 -3
  25. package/dist/types/client.d.ts +32 -5
  26. package/dist/types/index.d.ts +33 -5
  27. package/dist/uploader/core.js +41 -9
  28. package/dist/uploader/operations.js +19 -17
  29. package/dist/utils/config-loader.js +13 -6
  30. package/dist/utils/context.js +46 -76
  31. package/dist/utils/environment-config.js +20 -0
  32. package/dist/utils/output.js +8 -5
  33. package/dist/utils/screenshot-options.js +60 -10
  34. package/package.json +1 -1
  35. package/skills/vizzly/SKILL.md +12 -4
  36. package/skills/vizzly/references/cli-context.md +8 -2
  37. 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 buildLegacyServerInfo({
44
- pid,
45
- port,
46
- now = Date.now
47
- }) {
48
- return {
49
- pid,
50
- port: port.toString(),
51
- startTime: now()
52
- };
53
- }
54
- export function writeLegacyGlobalServerFile({
55
- pid,
56
- port
57
- }, {
58
- home = homedir,
59
- exists = existsSync,
60
- mkdir = mkdirSync,
61
- writeFile = writeFileSync,
62
- now = Date.now
63
- } = {}) {
64
- let globalVizzlyDir = join(home(), '.vizzly');
65
- if (!exists(globalVizzlyDir)) {
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
- legacyFileDeps = {}
82
+ cleanLocalFiles = true
109
83
  } = {}) {
110
- let localFiles = cleanupLocalDaemonFiles(directory, localFileDeps);
111
- let legacyGlobalServerFileRemoved = cleanupLegacyGlobalServerFile(legacyFileDeps);
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
- ...localFiles,
129
- legacyGlobalServerFileRemoved
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
- return await withTimeout(initPromise, timeoutMs, 'TDD server initialization timed out', timers);
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
- // Spawn child process with stdio inherited during init for direct error visibility
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', 'inherit', 'inherit', 'ipc'],
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
- output.hint('Stop with: vizzly tdd stop');
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
- // Most errors shown via inherited stdio, but catch any that weren't
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 port = options.port || 47392;
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
- cleanupDaemonState({
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
- cleanupDaemonState({
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
- export async function tddStatusCommand(_options, globalOptions = {}) {
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
- if (!existsSync(pidFile)) {
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 = parsePositiveInteger(readFileSync(pidFile, 'utf8'));
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 pid file is invalid (cleaning up stale files)');
637
- cleanupDaemonState();
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(serverInfo.port);
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.startTime) {
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(serverInfo.port);
668
-
669
- // JSON output for running status
670
- if (globalOptions.json) {
671
- output.data({
672
- running: true,
673
- port: serverInfo.port,
674
- pid,
675
- uptimeMs,
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
  }
@@ -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 branch = await detectBranch(options.branch);
89
- let commit = await detectCommit(options.commit);
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