@vizzly-testing/cli 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +11 -8
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
|
@@ -3,8 +3,247 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { getServerRegistry } from '../tdd/server-registry.js';
|
|
6
|
+
import { withTimeout } from '../utils/async-utils.js';
|
|
6
7
|
import * as output from '../utils/output.js';
|
|
7
8
|
import { tddCommand } from './tdd.js';
|
|
9
|
+
import { resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
|
|
10
|
+
export { findDaemonPidByPort, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
|
|
11
|
+
let defaultTimers = {
|
|
12
|
+
setTimeout,
|
|
13
|
+
clearTimeout
|
|
14
|
+
};
|
|
15
|
+
export function getLocalDaemonFiles(directory = process.cwd()) {
|
|
16
|
+
let vizzlyDir = join(directory, '.vizzly');
|
|
17
|
+
return {
|
|
18
|
+
vizzlyDir,
|
|
19
|
+
pidFile: join(vizzlyDir, 'server.pid'),
|
|
20
|
+
serverFile: join(vizzlyDir, 'server.json'),
|
|
21
|
+
logFile: join(vizzlyDir, 'server.log')
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function removeFileIfExists(filePath, deps = {}) {
|
|
25
|
+
let fileExists = deps.existsSync || existsSync;
|
|
26
|
+
let unlinkFile = deps.unlinkSync || unlinkSync;
|
|
27
|
+
if (fileExists(filePath)) {
|
|
28
|
+
unlinkFile(filePath);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export function cleanupLocalDaemonFiles(directory = process.cwd(), deps = {}) {
|
|
34
|
+
let {
|
|
35
|
+
pidFile,
|
|
36
|
+
serverFile
|
|
37
|
+
} = getLocalDaemonFiles(directory);
|
|
38
|
+
return {
|
|
39
|
+
pidFileRemoved: removeFileIfExists(pidFile, deps),
|
|
40
|
+
serverFileRemoved: removeFileIfExists(serverFile, deps)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
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
|
+
});
|
|
69
|
+
}
|
|
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
|
+
}
|
|
93
|
+
export function unregisterDaemonServer({
|
|
94
|
+
port,
|
|
95
|
+
directory = process.cwd(),
|
|
96
|
+
registry = getServerRegistry()
|
|
97
|
+
}) {
|
|
98
|
+
registry.unregister({
|
|
99
|
+
port,
|
|
100
|
+
directory
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function cleanupDaemonState({
|
|
104
|
+
port,
|
|
105
|
+
directory = process.cwd(),
|
|
106
|
+
registry = getServerRegistry(),
|
|
107
|
+
localFileDeps = {},
|
|
108
|
+
legacyFileDeps = {}
|
|
109
|
+
} = {}) {
|
|
110
|
+
let localFiles = cleanupLocalDaemonFiles(directory, localFileDeps);
|
|
111
|
+
let legacyGlobalServerFileRemoved = cleanupLegacyGlobalServerFile(legacyFileDeps);
|
|
112
|
+
try {
|
|
113
|
+
if (port !== undefined) {
|
|
114
|
+
unregisterDaemonServer({
|
|
115
|
+
port,
|
|
116
|
+
directory,
|
|
117
|
+
registry
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
registry.unregister({
|
|
121
|
+
directory
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Non-fatal; stale file cleanup is still useful on its own.
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...localFiles,
|
|
129
|
+
legacyGlobalServerFileRemoved
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function buildDashboardUrl(port = 47392) {
|
|
133
|
+
return `http://localhost:${port}`;
|
|
134
|
+
}
|
|
135
|
+
export function buildOpenDashboardCommand(url, platform = process.platform) {
|
|
136
|
+
if (platform === 'darwin') {
|
|
137
|
+
return {
|
|
138
|
+
command: 'open',
|
|
139
|
+
args: [url]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (platform === 'win32') {
|
|
143
|
+
return {
|
|
144
|
+
command: 'cmd',
|
|
145
|
+
args: ['/c', 'start', '', url]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
command: 'xdg-open',
|
|
150
|
+
args: [url]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function wait(ms, timers = defaultTimers) {
|
|
154
|
+
return new Promise(resolve => {
|
|
155
|
+
timers.setTimeout(resolve, ms);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export function buildDaemonChildArgs({
|
|
159
|
+
entrypoint = process.argv[1],
|
|
160
|
+
port,
|
|
161
|
+
options = {},
|
|
162
|
+
globalOptions = {}
|
|
163
|
+
}) {
|
|
164
|
+
return [entrypoint, 'tdd', 'start', '--daemon-child', '--port', port.toString(), ...(options.open ? ['--open'] : []), ...(options.baselineBuild ? ['--baseline-build', options.baselineBuild] : []), ...(options.baselineComparison ? ['--baseline-comparison', options.baselineComparison] : []), ...(options.environment ? ['--environment', options.environment] : []), ...(options.threshold !== undefined ? ['--threshold', options.threshold.toString()] : []), ...(options.minClusterSize !== undefined ? ['--min-cluster-size', options.minClusterSize.toString()] : []), ...(options.timeout ? ['--timeout', options.timeout] : []), ...(options.failOnDiff ? ['--fail-on-diff'] : []), ...(options.token ? ['--token', options.token] : []), ...(globalOptions.json ? ['--json'] : []), ...(globalOptions.verbose ? ['--verbose'] : []), ...(globalOptions.noColor ? ['--no-color'] : [])];
|
|
165
|
+
}
|
|
166
|
+
export function validateTddStartOptions(options = {}) {
|
|
167
|
+
let errors = [];
|
|
168
|
+
if (options.port) {
|
|
169
|
+
let port = Number(options.port);
|
|
170
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
171
|
+
errors.push('Port must be a valid number between 1 and 65535');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (options.timeout) {
|
|
175
|
+
let timeout = Number(options.timeout);
|
|
176
|
+
if (!Number.isInteger(timeout) || timeout < 1000) {
|
|
177
|
+
errors.push('Timeout must be at least 1000 milliseconds');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (options.threshold !== undefined) {
|
|
181
|
+
let threshold = Number(options.threshold);
|
|
182
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
183
|
+
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (options.minClusterSize !== undefined) {
|
|
187
|
+
let minClusterSize = Number(options.minClusterSize);
|
|
188
|
+
if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
|
|
189
|
+
errors.push('Min cluster size must be a positive integer');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return errors;
|
|
193
|
+
}
|
|
194
|
+
export async function waitForDaemonChildInit(child, {
|
|
195
|
+
timeoutMs = 30000,
|
|
196
|
+
timers = defaultTimers
|
|
197
|
+
} = {}) {
|
|
198
|
+
let cleanup = () => {};
|
|
199
|
+
let initPromise = new Promise(resolve => {
|
|
200
|
+
let handleDisconnect = () => {
|
|
201
|
+
cleanup();
|
|
202
|
+
resolve({
|
|
203
|
+
ok: true
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
let handleExit = () => {
|
|
207
|
+
cleanup();
|
|
208
|
+
resolve({
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: 'exit'
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
cleanup = () => {
|
|
214
|
+
child.off('disconnect', handleDisconnect);
|
|
215
|
+
child.off('exit', handleExit);
|
|
216
|
+
};
|
|
217
|
+
child.on('disconnect', handleDisconnect);
|
|
218
|
+
child.on('exit', handleExit);
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
return await withTimeout(initPromise, timeoutMs, 'TDD server initialization timed out', timers);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
cleanup();
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: 'timeout',
|
|
227
|
+
error
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export async function waitForServerRunning(port, {
|
|
232
|
+
maxAttempts = 10,
|
|
233
|
+
delayMs = 200,
|
|
234
|
+
isRunning = isServerRunning,
|
|
235
|
+
timers = defaultTimers
|
|
236
|
+
} = {}) {
|
|
237
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
238
|
+
if (await isRunning(port)) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (attempt < maxAttempts - 1) {
|
|
242
|
+
await wait(delayMs * (attempt + 1), timers);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
8
247
|
|
|
9
248
|
/**
|
|
10
249
|
* Start TDD server in daemon mode
|
|
@@ -33,7 +272,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
33
272
|
status: 'already_running',
|
|
34
273
|
port: existingServer.port,
|
|
35
274
|
pid: existingServer.pid,
|
|
36
|
-
dashboardUrl:
|
|
275
|
+
dashboardUrl: buildDashboardUrl(existingServer.port)
|
|
37
276
|
});
|
|
38
277
|
return;
|
|
39
278
|
}
|
|
@@ -50,14 +289,10 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
50
289
|
return;
|
|
51
290
|
} else {
|
|
52
291
|
// Stale entry - clean it up (registry and local files)
|
|
53
|
-
|
|
54
|
-
directory: process.cwd()
|
|
292
|
+
cleanupDaemonState({
|
|
293
|
+
directory: process.cwd(),
|
|
294
|
+
registry
|
|
55
295
|
});
|
|
56
|
-
let vizzlyDir = join(process.cwd(), '.vizzly');
|
|
57
|
-
let pidFile = join(vizzlyDir, 'server.pid');
|
|
58
|
-
let serverFile = join(vizzlyDir, 'server.json');
|
|
59
|
-
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
60
|
-
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
61
296
|
}
|
|
62
297
|
}
|
|
63
298
|
|
|
@@ -87,7 +322,9 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
87
322
|
}
|
|
88
323
|
try {
|
|
89
324
|
// Ensure .vizzly directory exists
|
|
90
|
-
let
|
|
325
|
+
let {
|
|
326
|
+
vizzlyDir
|
|
327
|
+
} = getLocalDaemonFiles();
|
|
91
328
|
if (!existsSync(vizzlyDir)) {
|
|
92
329
|
mkdirSync(vizzlyDir, {
|
|
93
330
|
recursive: true
|
|
@@ -103,47 +340,17 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
103
340
|
}
|
|
104
341
|
|
|
105
342
|
// Spawn child process with stdio inherited during init for direct error visibility
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
343
|
+
let child = spawn(process.execPath, buildDaemonChildArgs({
|
|
344
|
+
port,
|
|
345
|
+
options,
|
|
346
|
+
globalOptions
|
|
347
|
+
}), {
|
|
111
348
|
detached: true,
|
|
112
349
|
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
113
350
|
cwd: process.cwd()
|
|
114
351
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let initComplete = false;
|
|
118
|
-
let initFailed = false;
|
|
119
|
-
await new Promise(resolve => {
|
|
120
|
-
// Child disconnects IPC when initialization succeeds
|
|
121
|
-
child.on('disconnect', () => {
|
|
122
|
-
initComplete = true;
|
|
123
|
-
resolve();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Child exits before disconnecting = initialization failed
|
|
127
|
-
child.on('exit', () => {
|
|
128
|
-
if (!initComplete) {
|
|
129
|
-
initFailed = true;
|
|
130
|
-
resolve();
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Timeout after 30 seconds to prevent indefinite wait
|
|
135
|
-
const timeoutId = setTimeout(() => {
|
|
136
|
-
if (!initComplete && !initFailed) {
|
|
137
|
-
initFailed = true;
|
|
138
|
-
resolve();
|
|
139
|
-
}
|
|
140
|
-
}, 30000);
|
|
141
|
-
|
|
142
|
-
// Clear timeout if we resolve early
|
|
143
|
-
child.on('disconnect', () => clearTimeout(timeoutId));
|
|
144
|
-
child.on('exit', () => clearTimeout(timeoutId));
|
|
145
|
-
});
|
|
146
|
-
if (initFailed) {
|
|
352
|
+
let initResult = await waitForDaemonChildInit(child);
|
|
353
|
+
if (!initResult.ok) {
|
|
147
354
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
148
355
|
output.stopSpinner();
|
|
149
356
|
}
|
|
@@ -155,13 +362,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
155
362
|
child.unref();
|
|
156
363
|
|
|
157
364
|
// Verify server started with retries
|
|
158
|
-
|
|
159
|
-
const retryDelay = 200; // Start with 200ms
|
|
160
|
-
let running = false;
|
|
161
|
-
for (let i = 0; i < maxRetries && !running; i++) {
|
|
162
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
|
|
163
|
-
running = await isServerRunning(port);
|
|
164
|
-
}
|
|
365
|
+
let running = await waitForServerRunning(port);
|
|
165
366
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
166
367
|
output.stopSpinner();
|
|
167
368
|
}
|
|
@@ -178,14 +379,16 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
178
379
|
registry.cleanupStale();
|
|
179
380
|
|
|
180
381
|
// Register this server with log file path for menubar to read
|
|
181
|
-
let
|
|
382
|
+
let {
|
|
383
|
+
logFile
|
|
384
|
+
} = getLocalDaemonFiles();
|
|
182
385
|
registry.register({
|
|
183
386
|
pid: child.pid,
|
|
184
387
|
port: port,
|
|
185
388
|
directory: process.cwd(),
|
|
186
389
|
name: basename(process.cwd()),
|
|
187
390
|
startedAt: new Date().toISOString(),
|
|
188
|
-
logFile
|
|
391
|
+
logFile
|
|
189
392
|
});
|
|
190
393
|
} catch {
|
|
191
394
|
// Non-fatal
|
|
@@ -193,25 +396,16 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
193
396
|
|
|
194
397
|
// Also write legacy server.json for SDK discovery (backwards compatibility)
|
|
195
398
|
try {
|
|
196
|
-
|
|
197
|
-
if (!existsSync(globalVizzlyDir)) {
|
|
198
|
-
mkdirSync(globalVizzlyDir, {
|
|
199
|
-
recursive: true
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
const globalServerFile = join(globalVizzlyDir, 'server.json');
|
|
203
|
-
const serverInfo = {
|
|
399
|
+
writeLegacyGlobalServerFile({
|
|
204
400
|
pid: child.pid,
|
|
205
|
-
port
|
|
206
|
-
|
|
207
|
-
};
|
|
208
|
-
writeFileSync(globalServerFile, JSON.stringify(serverInfo, null, 2));
|
|
401
|
+
port
|
|
402
|
+
});
|
|
209
403
|
} catch {
|
|
210
404
|
// Non-fatal, SDK can still use health check
|
|
211
405
|
}
|
|
212
406
|
|
|
213
407
|
// JSON output for successful start
|
|
214
|
-
let dashboardUrl =
|
|
408
|
+
let dashboardUrl = buildDashboardUrl(port);
|
|
215
409
|
if (globalOptions.json) {
|
|
216
410
|
output.data({
|
|
217
411
|
status: 'started',
|
|
@@ -264,11 +458,12 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
264
458
|
* @private
|
|
265
459
|
*/
|
|
266
460
|
export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
461
|
+
let {
|
|
462
|
+
pidFile,
|
|
463
|
+
serverFile,
|
|
464
|
+
logFile
|
|
465
|
+
} = getLocalDaemonFiles();
|
|
466
|
+
let port = options.port || 47392;
|
|
272
467
|
|
|
273
468
|
// Configure output to write JSON logs to file (before tddCommand configures it)
|
|
274
469
|
output.configure({
|
|
@@ -279,7 +474,7 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
279
474
|
});
|
|
280
475
|
try {
|
|
281
476
|
// Use existing tddCommand but with daemon mode
|
|
282
|
-
|
|
477
|
+
let {
|
|
283
478
|
cleanup
|
|
284
479
|
} = await tddCommand(null,
|
|
285
480
|
// No test command - server only
|
|
@@ -294,43 +489,22 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
294
489
|
}
|
|
295
490
|
|
|
296
491
|
// Store our PID for the stop command
|
|
297
|
-
const pidFile = join(vizzlyDir, 'server.pid');
|
|
298
492
|
writeFileSync(pidFile, process.pid.toString());
|
|
299
|
-
|
|
493
|
+
let serverInfo = {
|
|
300
494
|
pid: process.pid,
|
|
301
495
|
port: port,
|
|
302
496
|
startTime: Date.now(),
|
|
303
497
|
failOnDiff: options.failOnDiff || false,
|
|
304
|
-
logFile
|
|
498
|
+
logFile
|
|
305
499
|
};
|
|
306
|
-
writeFileSync(
|
|
500
|
+
writeFileSync(serverFile, JSON.stringify(serverInfo, null, 2));
|
|
307
501
|
|
|
308
502
|
// Set up graceful shutdown
|
|
309
|
-
|
|
503
|
+
let handleShutdown = async () => {
|
|
310
504
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
315
|
-
|
|
316
|
-
// Unregister from global registry (for menubar app)
|
|
317
|
-
try {
|
|
318
|
-
let registry = getServerRegistry();
|
|
319
|
-
registry.unregister({
|
|
320
|
-
port: port,
|
|
321
|
-
directory: process.cwd()
|
|
322
|
-
});
|
|
323
|
-
} catch {
|
|
324
|
-
// Non-fatal
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Clean up legacy global server file
|
|
328
|
-
try {
|
|
329
|
-
const globalServerFile = join(homedir(), '.vizzly', 'server.json');
|
|
330
|
-
if (existsSync(globalServerFile)) unlinkSync(globalServerFile);
|
|
331
|
-
} catch {
|
|
332
|
-
// Non-fatal
|
|
333
|
-
}
|
|
505
|
+
cleanupDaemonState({
|
|
506
|
+
port
|
|
507
|
+
});
|
|
334
508
|
|
|
335
509
|
// Use the cleanup function from tddCommand
|
|
336
510
|
await cleanup();
|
|
@@ -364,50 +538,10 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
364
538
|
verbose: globalOptions.verbose,
|
|
365
539
|
color: !globalOptions.noColor
|
|
366
540
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// First try to find process by PID file
|
|
372
|
-
let pid = null;
|
|
373
|
-
if (existsSync(pidFile)) {
|
|
374
|
-
try {
|
|
375
|
-
pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
376
|
-
} catch {
|
|
377
|
-
// Invalid PID file
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// If no PID file or invalid, try to find by port using lsof
|
|
382
|
-
const port = options.port || 47392;
|
|
383
|
-
if (!pid) {
|
|
384
|
-
try {
|
|
385
|
-
const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
|
|
386
|
-
stdio: 'pipe'
|
|
387
|
-
});
|
|
388
|
-
let lsofOutput = '';
|
|
389
|
-
lsofProcess.stdout.on('data', data => {
|
|
390
|
-
lsofOutput += data.toString();
|
|
391
|
-
});
|
|
392
|
-
await new Promise(resolve => {
|
|
393
|
-
lsofProcess.on('close', code => {
|
|
394
|
-
if (code === 0 && lsofOutput.trim()) {
|
|
395
|
-
const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
|
|
396
|
-
if (foundPid && !Number.isNaN(foundPid)) {
|
|
397
|
-
pid = foundPid;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
resolve();
|
|
401
|
-
});
|
|
402
|
-
lsofProcess.on('error', () => {
|
|
403
|
-
// lsof not available, that's ok
|
|
404
|
-
resolve();
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
} catch {
|
|
408
|
-
// lsof failed, that's ok too
|
|
409
|
-
}
|
|
410
|
-
}
|
|
541
|
+
let port = options.port || 47392;
|
|
542
|
+
let pid = await resolveDaemonPid({
|
|
543
|
+
port
|
|
544
|
+
});
|
|
411
545
|
if (!pid) {
|
|
412
546
|
// JSON output for not running
|
|
413
547
|
if (globalOptions.json) {
|
|
@@ -420,46 +554,31 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
420
554
|
}
|
|
421
555
|
|
|
422
556
|
// Clean up any stale files
|
|
423
|
-
|
|
424
|
-
|
|
557
|
+
cleanupDaemonState({
|
|
558
|
+
port
|
|
559
|
+
});
|
|
425
560
|
return;
|
|
426
561
|
}
|
|
427
562
|
try {
|
|
428
|
-
let _colors = output.getColors();
|
|
429
|
-
|
|
430
563
|
// Try to kill the process gracefully
|
|
431
564
|
process.kill(pid, 'SIGTERM');
|
|
432
565
|
output.startSpinner('Stopping TDD server...');
|
|
433
|
-
|
|
434
|
-
// Give it a moment to shut down gracefully
|
|
435
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
566
|
+
let exited = await waitForProcessExit(pid);
|
|
436
567
|
|
|
437
568
|
// Check if it's still running
|
|
438
|
-
|
|
439
|
-
process.kill(pid, 0); // Just check if process exists
|
|
440
|
-
// If we get here, process is still running, force kill it
|
|
569
|
+
if (!exited) {
|
|
441
570
|
process.kill(pid, 'SIGKILL');
|
|
442
571
|
output.stopSpinner();
|
|
443
572
|
output.debug('tdd', 'Force killed process');
|
|
444
|
-
}
|
|
573
|
+
} else {
|
|
445
574
|
// Process is gone, which is what we want
|
|
446
575
|
output.stopSpinner();
|
|
447
576
|
}
|
|
448
577
|
|
|
449
578
|
// Clean up files
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// Unregister from global registry (for menubar app)
|
|
454
|
-
try {
|
|
455
|
-
let registry = getServerRegistry();
|
|
456
|
-
registry.unregister({
|
|
457
|
-
port: port,
|
|
458
|
-
directory: process.cwd()
|
|
459
|
-
});
|
|
460
|
-
} catch {
|
|
461
|
-
// Non-fatal
|
|
462
|
-
}
|
|
579
|
+
cleanupDaemonState({
|
|
580
|
+
port
|
|
581
|
+
});
|
|
463
582
|
|
|
464
583
|
// JSON output for successful stop
|
|
465
584
|
if (globalOptions.json) {
|
|
@@ -475,19 +594,9 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
475
594
|
if (error.code === 'ESRCH') {
|
|
476
595
|
// Process not found - clean up stale files
|
|
477
596
|
output.warn('TDD server was not running (cleaning up stale files)');
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Still unregister from registry
|
|
482
|
-
try {
|
|
483
|
-
let registry = getServerRegistry();
|
|
484
|
-
registry.unregister({
|
|
485
|
-
port: port,
|
|
486
|
-
directory: process.cwd()
|
|
487
|
-
});
|
|
488
|
-
} catch {
|
|
489
|
-
// Non-fatal
|
|
490
|
-
}
|
|
597
|
+
cleanupDaemonState({
|
|
598
|
+
port
|
|
599
|
+
});
|
|
491
600
|
} else {
|
|
492
601
|
output.error('Error stopping TDD server', error);
|
|
493
602
|
}
|
|
@@ -505,9 +614,10 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
505
614
|
verbose: globalOptions.verbose,
|
|
506
615
|
color: !globalOptions.noColor
|
|
507
616
|
});
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
617
|
+
let {
|
|
618
|
+
pidFile,
|
|
619
|
+
serverFile
|
|
620
|
+
} = getLocalDaemonFiles();
|
|
511
621
|
if (!existsSync(pidFile)) {
|
|
512
622
|
// JSON output for not running
|
|
513
623
|
if (globalOptions.json) {
|
|
@@ -521,7 +631,12 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
521
631
|
return;
|
|
522
632
|
}
|
|
523
633
|
try {
|
|
524
|
-
|
|
634
|
+
let pid = parsePositiveInteger(readFileSync(pidFile, 'utf8'));
|
|
635
|
+
if (!pid) {
|
|
636
|
+
output.warn('TDD server pid file is invalid (cleaning up stale files)');
|
|
637
|
+
cleanupDaemonState();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
525
640
|
|
|
526
641
|
// Check if process is actually running
|
|
527
642
|
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
@@ -534,22 +649,22 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
534
649
|
}
|
|
535
650
|
|
|
536
651
|
// Try to check health endpoint
|
|
537
|
-
|
|
652
|
+
let health = await checkServerHealth(serverInfo.port);
|
|
538
653
|
if (health.running) {
|
|
539
654
|
// Calculate uptime
|
|
540
655
|
let uptimeMs = null;
|
|
541
656
|
let uptimeStr = '';
|
|
542
657
|
if (serverInfo.startTime) {
|
|
543
658
|
uptimeMs = Date.now() - serverInfo.startTime;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
659
|
+
let uptime = Math.floor(uptimeMs / 1000);
|
|
660
|
+
let hours = Math.floor(uptime / 3600);
|
|
661
|
+
let minutes = Math.floor(uptime % 3600 / 60);
|
|
662
|
+
let seconds = uptime % 60;
|
|
548
663
|
if (hours > 0) uptimeStr += `${hours}h `;
|
|
549
664
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
550
665
|
uptimeStr += `${seconds}s`;
|
|
551
666
|
}
|
|
552
|
-
let dashboardUrl =
|
|
667
|
+
let dashboardUrl = buildDashboardUrl(serverInfo.port);
|
|
553
668
|
|
|
554
669
|
// JSON output for running status
|
|
555
670
|
if (globalOptions.json) {
|
|
@@ -589,10 +704,7 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
589
704
|
} catch (error) {
|
|
590
705
|
if (error.code === 'ESRCH') {
|
|
591
706
|
output.warn('TDD server process not found (cleaning up stale files)');
|
|
592
|
-
|
|
593
|
-
if (existsSync(serverFile)) {
|
|
594
|
-
unlinkSync(serverFile);
|
|
595
|
-
}
|
|
707
|
+
cleanupDaemonState();
|
|
596
708
|
} else {
|
|
597
709
|
output.error('Error checking TDD server status', error);
|
|
598
710
|
}
|
|
@@ -605,7 +717,7 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
605
717
|
*/
|
|
606
718
|
async function isServerRunning(port = 47392) {
|
|
607
719
|
try {
|
|
608
|
-
|
|
720
|
+
let health = await checkServerHealth(port);
|
|
609
721
|
return health.running;
|
|
610
722
|
} catch {
|
|
611
723
|
return false;
|
|
@@ -618,8 +730,8 @@ async function isServerRunning(port = 47392) {
|
|
|
618
730
|
*/
|
|
619
731
|
async function checkServerHealth(port = 47392) {
|
|
620
732
|
try {
|
|
621
|
-
|
|
622
|
-
|
|
733
|
+
let response = await fetch(`http://localhost:${port}/health`);
|
|
734
|
+
let data = await response.json();
|
|
623
735
|
return {
|
|
624
736
|
running: response.ok,
|
|
625
737
|
port: data.port,
|
|
@@ -637,18 +749,12 @@ async function checkServerHealth(port = 47392) {
|
|
|
637
749
|
* @private
|
|
638
750
|
*/
|
|
639
751
|
function openDashboard(port = 47392) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
} else if (process.platform === 'win32') {
|
|
647
|
-
openCmd = 'start';
|
|
648
|
-
} else {
|
|
649
|
-
openCmd = 'xdg-open';
|
|
650
|
-
}
|
|
651
|
-
spawn(openCmd, [url], {
|
|
752
|
+
let url = buildDashboardUrl(port);
|
|
753
|
+
let {
|
|
754
|
+
command,
|
|
755
|
+
args
|
|
756
|
+
} = buildOpenDashboardCommand(url);
|
|
757
|
+
spawn(command, args, {
|
|
652
758
|
detached: true,
|
|
653
759
|
stdio: 'ignore'
|
|
654
760
|
}).unref();
|