@vizzly-testing/cli 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +27 -14
  2. package/dist/cli.js +25 -1
  3. package/dist/client/index.js +77 -11
  4. package/dist/commands/init.js +23 -17
  5. package/dist/commands/tdd-daemon.js +312 -0
  6. package/dist/commands/tdd.js +45 -14
  7. package/dist/commands/upload.js +3 -1
  8. package/dist/reporter/reporter-bundle.css +1 -0
  9. package/dist/reporter/reporter-bundle.iife.js +57 -0
  10. package/dist/sdk/index.js +1 -1
  11. package/dist/server/handlers/api-handler.js +98 -30
  12. package/dist/server/handlers/tdd-handler.js +264 -77
  13. package/dist/server/http-server.js +358 -15
  14. package/dist/services/api-service.js +6 -1
  15. package/dist/services/html-report-generator.js +77 -0
  16. package/dist/services/report-generator/report.css +56 -0
  17. package/dist/services/screenshot-server.js +6 -3
  18. package/dist/services/server-manager.js +2 -9
  19. package/dist/services/tdd-service.js +188 -25
  20. package/dist/services/test-runner.js +43 -1
  21. package/dist/types/commands/tdd-daemon.d.ts +18 -0
  22. package/dist/types/container/index.d.ts +1 -3
  23. package/dist/types/reporter/src/components/app-router.d.ts +3 -0
  24. package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +5 -0
  25. package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +6 -0
  26. package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +6 -0
  27. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  28. package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +4 -0
  29. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +3 -0
  30. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +3 -0
  31. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +3 -0
  32. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +3 -0
  33. package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +16 -0
  34. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +5 -0
  35. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +4 -0
  36. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +8 -0
  37. package/dist/types/reporter/src/components/ui/smart-image.d.ts +7 -0
  38. package/dist/types/reporter/src/components/ui/status-badge.d.ts +5 -0
  39. package/dist/types/reporter/src/components/ui/toast.d.ts +4 -0
  40. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +6 -0
  41. package/dist/types/reporter/src/components/views/stats-view.d.ts +6 -0
  42. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +5 -0
  43. package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +20 -0
  44. package/dist/types/reporter/src/hooks/use-image-loader.d.ts +1 -0
  45. package/dist/types/reporter/src/hooks/use-report-data.d.ts +7 -0
  46. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +9 -0
  47. package/dist/types/reporter/src/main.d.ts +1 -0
  48. package/dist/types/reporter/src/services/api-client.d.ts +4 -0
  49. package/dist/types/reporter/src/utils/comparison-helpers.d.ts +16 -0
  50. package/dist/types/reporter/src/utils/constants.d.ts +37 -0
  51. package/dist/types/reporter/vite.config.d.ts +2 -0
  52. package/dist/types/reporter/vite.dev.config.d.ts +2 -0
  53. package/dist/types/sdk/index.d.ts +2 -3
  54. package/dist/types/server/handlers/api-handler.d.ts +5 -14
  55. package/dist/types/server/handlers/tdd-handler.d.ts +18 -17
  56. package/dist/types/server/http-server.d.ts +2 -1
  57. package/dist/types/services/base-service.d.ts +1 -2
  58. package/dist/types/services/html-report-generator.d.ts +3 -3
  59. package/dist/types/services/screenshot-server.d.ts +1 -1
  60. package/dist/types/services/server-manager.d.ts +25 -35
  61. package/dist/types/services/tdd-service.d.ts +7 -1
  62. package/dist/types/services/test-runner.d.ts +6 -1
  63. package/dist/types/utils/build-history.d.ts +16 -0
  64. package/dist/types/utils/config-loader.d.ts +1 -1
  65. package/dist/types/utils/console-ui.d.ts +1 -1
  66. package/dist/types/utils/git.d.ts +4 -4
  67. package/dist/types/utils/security.d.ts +2 -1
  68. package/dist/utils/build-history.js +103 -0
  69. package/dist/utils/config-loader.js +1 -1
  70. package/dist/utils/console-ui.js +2 -1
  71. package/dist/utils/environment-config.js +1 -1
  72. package/dist/utils/security.js +14 -5
  73. package/docs/api-reference.md +2 -4
  74. package/docs/doctor-command.md +1 -1
  75. package/docs/getting-started.md +1 -1
  76. package/docs/tdd-mode.md +176 -112
  77. package/package.json +17 -4
package/README.md CHANGED
@@ -55,7 +55,7 @@ vizzly upload ./screenshots --build-name "Release v1.2.3"
55
55
  vizzly run "npm test"
56
56
 
57
57
  # Use TDD mode for local development
58
- vizzly tdd "npm test"
58
+ vizzly tdd run "npm test"
59
59
  ```
60
60
 
61
61
  ### In your test code
@@ -125,29 +125,42 @@ vizzly run "npm test" --parallel-id "ci-run-123" # For parallel CI builds
125
125
  For local visual testing with immediate feedback, use the dedicated `tdd` command:
126
126
 
127
127
  ```bash
128
- # First run - creates local baselines
129
- vizzly tdd "npm test"
128
+ # Start interactive TDD dashboard
129
+ vizzly tdd start
130
130
 
131
- # Make changes and test - fails if visual differences detected
132
- vizzly tdd "npm test"
131
+ # Run your tests in watch mode
132
+ npm test -- --watch
133
133
 
134
- # Accept changes as new baseline
135
- vizzly tdd "npm test" --set-baseline
134
+ # View the dashboard at http://localhost:47392
136
135
  ```
137
136
 
138
- **Interactive HTML Report:** Each TDD run generates a detailed HTML report with visual comparison tools:
139
- - **Overlay mode** - Toggle between baseline and current screenshots
140
- - **Side-by-side mode** - Compare baseline and current images horizontally
141
- - **Onion skin mode** - Drag to reveal differences interactively
142
- - **Toggle mode** - Click to switch between baseline and current
137
+ **Interactive Dashboard:** The TDD dashboard provides real-time visual feedback:
138
+ - **Live Updates** - See comparisons as tests run
139
+ - **Visual Diff Modes** - Overlay, side-by-side, onion skin, and toggle views
140
+ - **Baseline Management** - Accept/reject changes directly from the UI
141
+ - **Test Statistics** - Real-time pass/fail metrics
142
+ - **Dark Theme** - Easy on the eyes during long sessions
143
+
144
+ **TDD Subcommands:**
145
+
146
+ ```bash
147
+ # Start the TDD dashboard server
148
+ vizzly tdd start [options]
149
+
150
+ # Run tests in single-shot mode
151
+ vizzly tdd run "npm test" [options]
152
+
153
+ # Stop a running TDD server
154
+ vizzly tdd stop
155
+ ```
143
156
 
144
157
  **TDD Command Options:**
145
158
  - `--set-baseline` - Accept current screenshots as new baseline
146
159
  - `--baseline-build <id>` - Use specific build as baseline (requires API token)
147
- - `--baseline-comparison <id>` - Use specific comparison as baseline (requires API token)
148
160
  - `--threshold <number>` - Comparison threshold (0-1, default: 0.1)
149
161
  - `--port <port>` - Server port (default: 47392)
150
162
  - `--timeout <ms>` - Server timeout (default: 30000)
163
+ - `--daemon` - Run server in background (start command only)
151
164
 
152
165
  ### Setup and Status Commands
153
166
  ```bash
@@ -368,7 +381,7 @@ Check if Vizzly is enabled in the current environment.
368
381
 
369
382
  ### Core Configuration
370
383
  - `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
371
- - `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
384
+ - `VIZZLY_API_URL`: Override API base URL. Default: `https://app.vizzly.dev`.
372
385
  - `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
373
386
 
374
387
  ### Parallel Builds
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { init } from './commands/init.js';
5
5
  import { uploadCommand, validateUploadOptions } from './commands/upload.js';
6
6
  import { runCommand, validateRunOptions } from './commands/run.js';
7
7
  import { tddCommand, validateTddOptions } from './commands/tdd.js';
8
+ import { tddStartCommand, tddStopCommand, tddStatusCommand } from './commands/tdd-daemon.js';
8
9
  import { statusCommand, validateStatusOptions } from './commands/status.js';
9
10
  import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
10
11
  import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
@@ -29,7 +30,30 @@ program.command('upload').description('Upload screenshots to Vizzly').argument('
29
30
  }
30
31
  await uploadCommand(path, options, globalOptions);
31
32
  });
32
- program.command('tdd').description('Run tests in TDD mode with local visual comparisons').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').option('--allow-no-token', 'Allow running without API token (no baselines)').action(async (command, options) => {
33
+
34
+ // TDD command with subcommands
35
+ const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
36
+
37
+ // TDD Start - Background server
38
+ tddCmd.command('start').description('Start background TDD server').option('--port <port>', 'Port for screenshot server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--token <token>', 'API token override').action(async options => {
39
+ const globalOptions = program.opts();
40
+ await tddStartCommand(options, globalOptions);
41
+ });
42
+
43
+ // TDD Stop - Kill background server
44
+ tddCmd.command('stop').description('Stop background TDD server').action(async options => {
45
+ const globalOptions = program.opts();
46
+ await tddStopCommand(options, globalOptions);
47
+ });
48
+
49
+ // TDD Status - Check server status
50
+ tddCmd.command('status').description('Check TDD server status').action(async options => {
51
+ const globalOptions = program.opts();
52
+ await tddStatusCommand(options, globalOptions);
53
+ });
54
+
55
+ // TDD Run - One-off test run (primary workflow)
56
+ tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for screenshot server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').action(async (command, options) => {
33
57
  const globalOptions = program.opts();
34
58
 
35
59
  // Validate options
@@ -3,11 +3,14 @@
3
3
  * @description Thin client for test runners - minimal API for taking screenshots
4
4
  */
5
5
 
6
- import { isVizzlyEnabled, getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
6
+ import { getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { join, parse, dirname } from 'path';
7
9
 
8
10
  // Internal client state
9
11
  let currentClient = null;
10
12
  let isDisabled = false;
13
+ let hasLoggedWarning = false;
11
14
 
12
15
  /**
13
16
  * Check if Vizzly is currently disabled
@@ -15,7 +18,8 @@ let isDisabled = false;
15
18
  * @returns {boolean} True if disabled via environment variable or auto-disabled due to failure
16
19
  */
17
20
  function isVizzlyDisabled() {
18
- return !isVizzlyEnabled() || isDisabled;
21
+ // Don't check isVizzlyEnabled() here - let auto-discovery happen first
22
+ return isDisabled;
19
23
  }
20
24
 
21
25
  /**
@@ -31,6 +35,37 @@ function disableVizzly(reason = 'disabled') {
31
35
  }
32
36
  }
33
37
 
38
+ /**
39
+ * Auto-discover local TDD server by checking for server.json
40
+ * @private
41
+ * @returns {string|null} Server URL if found
42
+ */
43
+ function autoDiscoverTddServer() {
44
+ try {
45
+ // Look for .vizzly/server.json in current directory and parent directories
46
+ let currentDir = process.cwd();
47
+ const root = parse(currentDir).root;
48
+ while (currentDir !== root) {
49
+ const serverJsonPath = join(currentDir, '.vizzly', 'server.json');
50
+ if (existsSync(serverJsonPath)) {
51
+ try {
52
+ const serverInfo = JSON.parse(readFileSync(serverJsonPath, 'utf8'));
53
+ if (serverInfo.port) {
54
+ const url = `http://localhost:${serverInfo.port}`;
55
+ return url;
56
+ }
57
+ } catch {
58
+ // Invalid JSON, continue searching
59
+ }
60
+ }
61
+ currentDir = dirname(currentDir);
62
+ }
63
+ return null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
34
69
  /**
35
70
  * Get the current client instance
36
71
  * @private
@@ -40,9 +75,19 @@ function getClient() {
40
75
  return null;
41
76
  }
42
77
  if (!currentClient) {
43
- // Only try to initialize if VIZZLY_ENABLED is explicitly true
44
- const serverUrl = getServerUrl();
45
- if (serverUrl && isVizzlyEnabled()) {
78
+ let serverUrl = getServerUrl();
79
+
80
+ // Auto-detect local TDD server and enable Vizzly if TDD server is found
81
+ if (!serverUrl) {
82
+ serverUrl = autoDiscoverTddServer();
83
+ if (serverUrl) {
84
+ // Automatically enable Vizzly when TDD server is detected
85
+ setVizzlyEnabled(true);
86
+ }
87
+ }
88
+
89
+ // If we have a server URL, create the client (regardless of initial enabled state)
90
+ if (serverUrl) {
46
91
  currentClient = createSimpleClient(serverUrl);
47
92
  }
48
93
  }
@@ -80,22 +125,39 @@ function createSimpleClient(serverUrl) {
80
125
  };
81
126
  });
82
127
 
83
- // In TDD mode, if we get 422 (visual difference), throw with clean message
128
+ // In TDD mode, if we get 422 (visual difference), log but DON'T throw
129
+ // This allows all screenshots in the test to be captured and compared
84
130
  if (response.status === 422 && errorData.tddMode && errorData.comparison) {
85
131
  const comp = errorData.comparison;
86
- throw new Error(`Visual difference detected in "${name}"\n` + ` Baseline: ${comp.baseline}\n` + ` Current: ${comp.current}\n` + ` Diff: ${comp.diff}`);
132
+ const diffPercent = comp.diffPercentage ? comp.diffPercentage.toFixed(2) : '0.00';
133
+
134
+ // Extract port from serverUrl (e.g., "http://localhost:47392" -> "47392")
135
+ const urlMatch = serverUrl.match(/:(\d+)/);
136
+ const port = urlMatch ? urlMatch[1] : '47392';
137
+ const dashboardUrl = `http://localhost:${port}/dashboard`;
138
+
139
+ // Just log warning - don't throw by default in TDD mode
140
+ // This allows all screenshots to be captured
141
+ console.warn(`⚠️ Visual diff: ${comp.name} (${diffPercent}%) → ${dashboardUrl}`);
142
+
143
+ // Return success so test continues and captures remaining screenshots
144
+ return {
145
+ success: true,
146
+ status: 'failed',
147
+ name: comp.name,
148
+ diffPercentage: comp.diffPercentage
149
+ };
87
150
  }
88
151
  throw new Error(`Screenshot failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
89
152
  }
90
153
  return await response.json();
91
154
  } catch (error) {
92
155
  // In TDD mode with visual differences, throw the error to fail the test
93
- if (error.message.includes('Visual difference detected')) {
156
+ if (error.message.toLowerCase().includes('visual diff')) {
94
157
  // Clean output for TDD mode - don't spam with additional logs
95
158
  throw error;
96
159
  }
97
- console.error(`Failed to save screenshot "${name}":`, error.message);
98
- console.error(`Vizzly screenshot failed for ${name}: ${error.message}`);
160
+ console.error(`Vizzly screenshot failed for ${name}:`, error.message);
99
161
  if (error.message.includes('fetch') || error.code === 'ECONNREFUSED') {
100
162
  console.error(`Server URL: ${serverUrl}/screenshot`);
101
163
  console.error('This usually means the Vizzly server is not running or not accessible');
@@ -157,7 +219,11 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
157
219
  }
158
220
  const client = getClient();
159
221
  if (!client) {
160
- console.warn('Vizzly client not initialized. Screenshots will be skipped.');
222
+ if (!hasLoggedWarning) {
223
+ console.warn('Vizzly client not initialized. Screenshots will be skipped.');
224
+ hasLoggedWarning = true;
225
+ disableVizzly();
226
+ }
161
227
  return;
162
228
  }
163
229
  return client.screenshot(name, imageBuffer, options);
@@ -40,30 +40,36 @@ export class InitCommand {
40
40
  const configContent = `export default {
41
41
  // API configuration
42
42
  // Set VIZZLY_TOKEN environment variable or uncomment and set here:
43
- // apiToken: 'your-token-here',
44
-
45
- // Screenshot configuration
46
- screenshots: {
47
- directory: './screenshots',
48
- formats: ['png']
49
- },
50
-
51
- // Server configuration
43
+ // apiKey: 'your-token-here',
44
+
45
+ // Server configuration (for run command)
52
46
  server: {
53
47
  port: 47392,
48
+ timeout: 30000,
54
49
  screenshotPath: '/screenshot'
55
50
  },
56
-
57
- // Comparison configuration
58
- comparison: {
59
- threshold: 0.1,
60
- ignoreAntialiasing: true
51
+
52
+ // Build configuration
53
+ build: {
54
+ name: 'Build {timestamp}',
55
+ environment: 'test'
61
56
  },
62
-
63
- // Upload configuration
57
+
58
+ // Upload configuration (for upload command)
64
59
  upload: {
65
- concurrency: 5,
60
+ screenshotsDir: './screenshots',
61
+ batchSize: 10,
66
62
  timeout: 30000
63
+ },
64
+
65
+ // Comparison configuration
66
+ comparison: {
67
+ threshold: 0.1
68
+ },
69
+
70
+ // TDD configuration
71
+ tdd: {
72
+ openReport: false // Whether to auto-open HTML report in browser
67
73
  }
68
74
  };
69
75
  `;
@@ -0,0 +1,312 @@
1
+ import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { ConsoleUI } from '../utils/console-ui.js';
5
+ import { tddCommand } from './tdd.js';
6
+
7
+ /**
8
+ * Start TDD server in daemon mode
9
+ * @param {Object} options - Command options
10
+ * @param {Object} globalOptions - Global CLI options
11
+ */
12
+ export async function tddStartCommand(options = {}, globalOptions = {}) {
13
+ const ui = new ConsoleUI({
14
+ json: globalOptions.json,
15
+ verbose: globalOptions.verbose,
16
+ color: !globalOptions.noColor
17
+ });
18
+
19
+ // Check if server already running
20
+ if (await isServerRunning(options.port || 47392)) {
21
+ const port = options.port || 47392;
22
+ ui.info(`TDD server already running at http://localhost:${port}`);
23
+ ui.info(`Dashboard: http://localhost:${port}/dashboard`);
24
+ if (options.open) {
25
+ openDashboard(port);
26
+ }
27
+ return;
28
+ }
29
+ try {
30
+ // Ensure .vizzly directory exists
31
+ const vizzlyDir = join(process.cwd(), '.vizzly');
32
+ if (!existsSync(vizzlyDir)) {
33
+ mkdirSync(vizzlyDir, {
34
+ recursive: true
35
+ });
36
+ }
37
+ const port = options.port || 47392;
38
+
39
+ // Use existing tddCommand but with daemon mode - this will start and keep running
40
+ const {
41
+ cleanup
42
+ } = await tddCommand(null,
43
+ // No test command - server only
44
+ {
45
+ ...options,
46
+ daemon: true // Flag to indicate daemon mode
47
+ }, globalOptions);
48
+
49
+ // The server is now running in this process
50
+ // Store our PID for the stop command
51
+ const pidFile = join(vizzlyDir, 'server.pid');
52
+ writeFileSync(pidFile, process.pid.toString());
53
+ const serverInfo = {
54
+ pid: process.pid,
55
+ port: port,
56
+ startTime: Date.now()
57
+ };
58
+ writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
59
+ ui.success(`TDD server started at http://localhost:${port}`);
60
+ ui.info('');
61
+ ui.info('Dashboard URLs:');
62
+ ui.info(` Comparisons: http://localhost:${port}/`);
63
+ ui.info(` Stats: http://localhost:${port}/stats`);
64
+ ui.info('');
65
+ ui.info('Next steps:');
66
+ ui.info(' 1. Run your tests (any test runner)');
67
+ ui.info(' 2. Open the dashboard in your browser');
68
+ ui.info(' 3. Manage baselines in the Stats view');
69
+ ui.info('');
70
+ ui.info('Stop server: npx vizzly tdd stop');
71
+ if (options.open) {
72
+ openDashboard(port);
73
+ }
74
+
75
+ // Set up graceful shutdown
76
+ const handleShutdown = async signal => {
77
+ ui.info(`\nReceived ${signal}, shutting down gracefully...`);
78
+ try {
79
+ // Clean up PID files
80
+ if (existsSync(pidFile)) unlinkSync(pidFile);
81
+ const serverFile = join(vizzlyDir, 'server.json');
82
+ if (existsSync(serverFile)) unlinkSync(serverFile);
83
+
84
+ // Use the cleanup function from tddCommand
85
+ await cleanup();
86
+ ui.success('TDD server stopped');
87
+ } catch (error) {
88
+ ui.error('Error during shutdown:', error);
89
+ }
90
+ process.exit(0);
91
+ };
92
+
93
+ // Register signal handlers
94
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
95
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
96
+
97
+ // Keep process alive
98
+ process.stdin.resume();
99
+ } catch (error) {
100
+ ui.error('Failed to start TDD daemon', error);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Stop TDD daemon server
107
+ * @param {Object} options - Command options
108
+ * @param {Object} globalOptions - Global CLI options
109
+ */
110
+ export async function tddStopCommand(options = {}, globalOptions = {}) {
111
+ const ui = new ConsoleUI({
112
+ json: globalOptions.json,
113
+ verbose: globalOptions.verbose,
114
+ color: !globalOptions.noColor
115
+ });
116
+ const vizzlyDir = join(process.cwd(), '.vizzly');
117
+ const pidFile = join(vizzlyDir, 'server.pid');
118
+ const serverFile = join(vizzlyDir, 'server.json');
119
+
120
+ // First try to find process by PID file
121
+ let pid = null;
122
+ if (existsSync(pidFile)) {
123
+ try {
124
+ pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
125
+ } catch {
126
+ // Invalid PID file
127
+ }
128
+ }
129
+
130
+ // If no PID file or invalid, try to find by port using lsof
131
+ const port = options.port || 47392;
132
+ if (!pid) {
133
+ try {
134
+ const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
135
+ stdio: 'pipe'
136
+ });
137
+ let lsofOutput = '';
138
+ lsofProcess.stdout.on('data', data => {
139
+ lsofOutput += data.toString();
140
+ });
141
+ await new Promise(resolve => {
142
+ lsofProcess.on('close', code => {
143
+ if (code === 0 && lsofOutput.trim()) {
144
+ const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
145
+ if (foundPid && !isNaN(foundPid)) {
146
+ pid = foundPid;
147
+ }
148
+ }
149
+ resolve();
150
+ });
151
+ lsofProcess.on('error', () => {
152
+ // lsof not available, that's ok
153
+ resolve();
154
+ });
155
+ });
156
+ } catch {
157
+ // lsof failed, that's ok too
158
+ }
159
+ }
160
+ if (!pid) {
161
+ ui.warning('No TDD server running');
162
+
163
+ // Clean up any stale files
164
+ if (existsSync(pidFile)) unlinkSync(pidFile);
165
+ if (existsSync(serverFile)) unlinkSync(serverFile);
166
+ return;
167
+ }
168
+ try {
169
+ // Try to kill the process gracefully
170
+ process.kill(pid, 'SIGTERM');
171
+ ui.info(`Stopping TDD server (PID: ${pid})...`);
172
+
173
+ // Give it a moment to shut down gracefully
174
+ await new Promise(resolve => setTimeout(resolve, 2000));
175
+
176
+ // Check if it's still running
177
+ try {
178
+ process.kill(pid, 0); // Just check if process exists
179
+ // If we get here, process is still running, force kill it
180
+ process.kill(pid, 'SIGKILL');
181
+ ui.info('Force killed TDD server');
182
+ } catch {
183
+ // Process is gone, which is what we want
184
+ }
185
+
186
+ // Clean up files
187
+ if (existsSync(pidFile)) unlinkSync(pidFile);
188
+ if (existsSync(serverFile)) unlinkSync(serverFile);
189
+ ui.success('TDD server stopped');
190
+ } catch (error) {
191
+ if (error.code === 'ESRCH') {
192
+ // Process not found - clean up stale files
193
+ ui.warning('TDD server was not running (cleaning up stale files)');
194
+ if (existsSync(pidFile)) unlinkSync(pidFile);
195
+ if (existsSync(serverFile)) unlinkSync(serverFile);
196
+ } else {
197
+ ui.error('Error stopping TDD server', error);
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Check TDD daemon server status
204
+ * @param {Object} options - Command options
205
+ * @param {Object} globalOptions - Global CLI options
206
+ */
207
+ export async function tddStatusCommand(options, globalOptions = {}) {
208
+ const ui = new ConsoleUI({
209
+ json: globalOptions.json,
210
+ verbose: globalOptions.verbose,
211
+ color: !globalOptions.noColor
212
+ });
213
+ const vizzlyDir = join(process.cwd(), '.vizzly');
214
+ const pidFile = join(vizzlyDir, 'server.pid');
215
+ const serverFile = join(vizzlyDir, 'server.json');
216
+ if (!existsSync(pidFile)) {
217
+ ui.info('TDD server not running');
218
+ return;
219
+ }
220
+ try {
221
+ const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
222
+
223
+ // Check if process is actually running
224
+ process.kill(pid, 0); // Signal 0 just checks if process exists
225
+
226
+ let serverInfo = {
227
+ port: 47392
228
+ };
229
+ if (existsSync(serverFile)) {
230
+ serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
231
+ }
232
+
233
+ // Try to check health endpoint
234
+ const health = await checkServerHealth(serverInfo.port);
235
+ if (health.running) {
236
+ ui.success(`TDD server running (PID: ${pid})`);
237
+ ui.info(`Server: http://localhost:${serverInfo.port}`);
238
+ ui.info(`Dashboard: http://localhost:${serverInfo.port}/dashboard`);
239
+ if (serverInfo.startTime) {
240
+ const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
241
+ ui.info(`Uptime: ${uptime} seconds`);
242
+ }
243
+ } else {
244
+ ui.warning('TDD server process exists but not responding to health checks');
245
+ }
246
+ } catch (error) {
247
+ if (error.code === 'ESRCH') {
248
+ ui.warning('TDD server process not found (cleaning up stale files)');
249
+ unlinkSync(pidFile);
250
+ if (existsSync(serverFile)) {
251
+ unlinkSync(serverFile);
252
+ }
253
+ } else {
254
+ ui.error('Error checking TDD server status', error);
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Check if server is running on given port
261
+ * @private
262
+ */
263
+ async function isServerRunning(port = 47392) {
264
+ try {
265
+ const health = await checkServerHealth(port);
266
+ return health.running;
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Check server health endpoint
274
+ * @private
275
+ */
276
+ async function checkServerHealth(port = 47392) {
277
+ try {
278
+ const response = await fetch(`http://localhost:${port}/health`);
279
+ const data = await response.json();
280
+ return {
281
+ running: response.ok,
282
+ port: data.port,
283
+ uptime: data.uptime
284
+ };
285
+ } catch {
286
+ return {
287
+ running: false
288
+ };
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Open dashboard in default browser
294
+ * @private
295
+ */
296
+ function openDashboard(port = 47392) {
297
+ const url = `http://localhost:${port}/dashboard`;
298
+
299
+ // Cross-platform open command
300
+ let openCmd;
301
+ if (process.platform === 'darwin') {
302
+ openCmd = 'open';
303
+ } else if (process.platform === 'win32') {
304
+ openCmd = 'start';
305
+ } else {
306
+ openCmd = 'xdg-open';
307
+ }
308
+ spawn(openCmd, [url], {
309
+ detached: true,
310
+ stdio: 'ignore'
311
+ }).unref();
312
+ }