@testspectra/cli 1.1.8-rc.8 → 1.1.10

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 (88) hide show
  1. package/README.md +11 -8
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/commands/__tests__/doctor-filter.test.d.ts +1 -0
  4. package/dist/commands/__tests__/doctor-filter.test.js +37 -0
  5. package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
  6. package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
  7. package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
  8. package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
  9. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  10. package/dist/commands/__tests__/upgrade.test.js +95 -0
  11. package/dist/commands/add.js +5 -4
  12. package/dist/commands/devices.js +1 -0
  13. package/dist/commands/docs.d.ts +4 -0
  14. package/dist/commands/docs.js +17 -0
  15. package/dist/commands/doctor.d.ts +9 -1
  16. package/dist/commands/doctor.js +17 -3
  17. package/dist/commands/init.js +7 -160
  18. package/dist/commands/install.js +5 -0
  19. package/dist/commands/license.js +1 -0
  20. package/dist/commands/run.d.ts +2 -0
  21. package/dist/commands/run.js +22 -0
  22. package/dist/commands/test.d.ts +13 -0
  23. package/dist/commands/test.js +115 -21
  24. package/dist/commands/upgrade.d.ts +12 -0
  25. package/dist/commands/upgrade.js +197 -0
  26. package/dist/config/schema.d.ts +53 -0
  27. package/dist/config/schema.js +2 -0
  28. package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
  29. package/dist/generator/__tests__/type-generator.test.js +38 -0
  30. package/dist/generator/architecture-doc-generator.d.ts +14 -0
  31. package/dist/generator/architecture-doc-generator.js +80 -0
  32. package/dist/generator/index.d.ts +1 -0
  33. package/dist/generator/index.js +1 -0
  34. package/dist/generator/tsconfig-generator.js +1 -1
  35. package/dist/generator/type-generator.d.ts +5 -2
  36. package/dist/generator/type-generator.js +14 -17
  37. package/dist/index.js +59 -16
  38. package/dist/linter/__tests__/shared-lib-boundary.test.js +15 -15
  39. package/dist/linter/discovery.d.ts +3 -3
  40. package/dist/linter/discovery.js +7 -7
  41. package/dist/linter/schemas/spec.schema.d.ts +6 -6
  42. package/dist/runner/bridge.d.ts +1 -1
  43. package/dist/runner/bridge.js +67 -16
  44. package/dist/runner/reporter.d.ts +1 -0
  45. package/dist/runner/reporter.js +12 -6
  46. package/dist/utils/__tests__/dev-server-manager.test.d.ts +1 -0
  47. package/dist/utils/__tests__/dev-server-manager.test.js +72 -0
  48. package/dist/utils/background-update-check.d.ts +1 -0
  49. package/dist/utils/background-update-check.js +43 -0
  50. package/dist/utils/dependency-updates.d.ts +15 -0
  51. package/dist/utils/dependency-updates.js +107 -0
  52. package/dist/utils/dev-server-manager.d.ts +40 -0
  53. package/dist/utils/dev-server-manager.js +257 -0
  54. package/dist/utils/update-notifier.d.ts +2 -0
  55. package/dist/utils/update-notifier.js +67 -0
  56. package/package.json +10 -9
  57. package/templates/default/package.json +2 -2
  58. package/templates/docs/ARCHITECTURE.default.md +26 -0
  59. package/templates/docs/ARCHITECTURE.nx.md +147 -0
  60. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  61. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  62. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  63. package/templates/nx/.nx/workspace-data/d/daemon.log +947 -0
  64. package/templates/nx/.nx/workspace-data/d/server-process.json +1 -1
  65. package/templates/nx/.nx/workspace-data/file-map.json +172 -186
  66. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  67. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  68. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +81 -130
  69. package/templates/nx/.nx/workspace-data/project-graph.json +90 -144
  70. package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
  71. package/templates/nx/package.json +3 -3
  72. package/templates/nx/packages/matchers/e2e/project.json +2 -2
  73. package/templates/nx/packages/playground/e2e/project.json +2 -2
  74. package/templates/nx/pnpm-lock.yaml +191 -214
  75. package/templates/nx/shared/testing/project.json +1 -2
  76. package/templates/nx/spectra.config.ts +13 -0
  77. package/templates/react-component/package.json +3 -3
  78. package/templates/react-component/page-objects/BadgeComponent/web.ts +4 -4
  79. package/templates/react-component/page-objects/ButtonComponent/web.ts +3 -3
  80. package/templates/react-component/page-objects/InputComponent/web.ts +1 -1
  81. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +0 -3
  82. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +0 -9
  83. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +1 -1
  84. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +4 -8
  85. package/templates/react-component/spectra.config.ts +0 -10
  86. package/templates/react-component/src/components/Badge/Badge.tsx +3 -3
  87. package/templates/react-component/src/components/Button/Button.tsx +5 -3
  88. package/templates/react-component/src/components/Input/Input.tsx +2 -3
@@ -16,16 +16,54 @@ function platformPackageName() {
16
16
  return '@testspectra/cli-darwin-arm64';
17
17
  if (platform === 'linux' && arch === 'x64')
18
18
  return '@testspectra/cli-linux-x64';
19
- if (platform === 'win32' && arch === 'arm64')
20
- return '@testspectra/cli-win32-arm64';
21
19
  if (platform === 'win32' && arch === 'x64')
22
20
  return '@testspectra/cli-win32-x64';
21
+ // win32/arm64 has no published native binary (scripts/release-cli-local.sh no longer builds
22
+ // it) — falls through to `pkgName === null` below, which throws a clear "not currently
23
+ // supported" error instead of pointing at an optional dependency that doesn't exist.
23
24
  return null;
24
25
  }
25
26
  export class RustCoreBridge {
26
27
  static resolveBinaryPath() {
27
28
  const isWindows = process.platform === 'win32';
28
29
  const binaryName = isWindows ? 'testspectra-runner.exe' : 'testspectra-runner';
30
+ // In local monorepo development (where core/Cargo.toml exists), prioritize the locally-built
31
+ // binary in core/target/release or cli/bin over any stale optionalDependency in node_modules.
32
+ let workspaceRoot = null;
33
+ let curr = __dirname;
34
+ while (curr !== path.dirname(curr)) {
35
+ if (fs.existsSync(path.join(curr, 'core/Cargo.toml')) || fs.existsSync(path.join(curr, 'pnpm-workspace.yaml'))) {
36
+ workspaceRoot = curr;
37
+ break;
38
+ }
39
+ curr = path.dirname(curr);
40
+ }
41
+ const candidatePaths = [
42
+ ...(workspaceRoot
43
+ ? [
44
+ path.join(workspaceRoot, 'core/target/release', binaryName),
45
+ path.join(workspaceRoot, 'target/release', binaryName),
46
+ path.join(workspaceRoot, 'cli/bin', binaryName),
47
+ path.join(workspaceRoot, 'core/cli/target/release', binaryName),
48
+ path.join(workspaceRoot, 'core/target/debug', binaryName),
49
+ path.join(workspaceRoot, 'target/debug', binaryName),
50
+ path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
51
+ ]
52
+ : []),
53
+ path.join(__dirname, '../bin', binaryName),
54
+ path.join(__dirname, '../../bin', binaryName),
55
+ path.join(__dirname, '../../bin/testspectra-runner'),
56
+ ];
57
+ if (workspaceRoot) {
58
+ for (const p of candidatePaths) {
59
+ if (fs.existsSync(p)) {
60
+ return p;
61
+ }
62
+ if (fs.existsSync(`${p}.exe`)) {
63
+ return `${p}.exe`;
64
+ }
65
+ }
66
+ }
29
67
  // 1. Production install: resolve the prebuilt binary from the platform-specific
30
68
  // optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
31
69
  // published npm package ships native binaries for every supported platform without
@@ -36,6 +74,15 @@ export class RustCoreBridge {
36
74
  const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
37
75
  const pkgBinPath = path.join(path.dirname(pkgJsonPath), 'bin', binaryName);
38
76
  if (fs.existsSync(pkgBinPath)) {
77
+ if (process.platform !== 'win32') {
78
+ try {
79
+ const stat = fs.statSync(pkgBinPath);
80
+ if ((stat.mode & 0o111) === 0) {
81
+ fs.chmodSync(pkgBinPath, 0o755);
82
+ }
83
+ }
84
+ catch { }
85
+ }
39
86
  return pkgBinPath;
40
87
  }
41
88
  }
@@ -43,20 +90,7 @@ export class RustCoreBridge {
43
90
  // Optional dependency not installed — fall through to the workspace dev paths below.
44
91
  }
45
92
  }
46
- // 2. Monorepo dev fallback: look for a precompiled release/debug binary in the workspace
47
- // target, as produced by scripts/ensure-runner-binary.js during local development.
48
- const workspaceRoot = path.resolve(__dirname, '../../..');
49
- const candidatePaths = [
50
- path.join(workspaceRoot, 'core/target/release', binaryName),
51
- path.join(workspaceRoot, 'target/release', binaryName),
52
- path.join(workspaceRoot, 'cli/bin', binaryName),
53
- path.join(__dirname, '../../bin', binaryName),
54
- path.join(workspaceRoot, 'core/cli/target/release', binaryName),
55
- path.join(workspaceRoot, 'core/target/debug', binaryName),
56
- path.join(workspaceRoot, 'target/debug', binaryName),
57
- path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
58
- path.join(__dirname, '../../bin/testspectra-runner'),
59
- ];
93
+ // 2. Dev fallback if not already resolved above
60
94
  for (const p of candidatePaths) {
61
95
  if (fs.existsSync(p)) {
62
96
  return p;
@@ -105,9 +139,12 @@ export class RustCoreBridge {
105
139
  return new Promise((resolve, reject) => {
106
140
  const child = spawn(binPath, [payload], {
107
141
  env: runnerEnv,
142
+ windowsHide: true,
108
143
  });
109
144
  let runStatus = 'failed';
110
145
  let runDuration = '0s';
146
+ let completePassedCount;
147
+ let completeFailedCount;
111
148
  child.stdout.on('data', (data) => {
112
149
  const lines = data.toString().split('\n');
113
150
  for (const line of lines) {
@@ -150,6 +187,12 @@ export class RustCoreBridge {
150
187
  else if (parsed.type === 'complete') {
151
188
  runStatus = parsed.status;
152
189
  runDuration = parsed.duration;
190
+ if (typeof parsed.passed_count === 'number') {
191
+ completePassedCount = parsed.passed_count;
192
+ }
193
+ if (typeof parsed.failed_count === 'number') {
194
+ completeFailedCount = parsed.failed_count;
195
+ }
153
196
  }
154
197
  else if (parsed.type === 'error') {
155
198
  reporter.addLog({
@@ -185,6 +228,14 @@ export class RustCoreBridge {
185
228
  });
186
229
  child.on('close', (code) => {
187
230
  let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
231
+ if (result.passedCount === 0 && result.failedCount === 0) {
232
+ if (completePassedCount !== undefined) {
233
+ result.passedCount = completePassedCount;
234
+ }
235
+ if (completeFailedCount !== undefined) {
236
+ result.failedCount = completeFailedCount;
237
+ }
238
+ }
188
239
  // Only trust a pre-existing outputJsonPath when it was actually written during THIS run
189
240
  // (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
190
241
  // native runner writes its own complete report once the pipeline reaches the report
@@ -35,6 +35,7 @@ export declare class Reporter {
35
35
  private networkEvents;
36
36
  private pendingTests;
37
37
  private workerToTest;
38
+ private pendingErrorMsg?;
38
39
  private passedCount;
39
40
  private failedCount;
40
41
  private isInteractive;
@@ -155,6 +155,7 @@ export class Reporter {
155
155
  networkEvents = [];
156
156
  pendingTests = new Map();
157
157
  workerToTest = new Map();
158
+ pendingErrorMsg;
158
159
  passedCount = 0;
159
160
  failedCount = 0;
160
161
  isInteractive;
@@ -355,10 +356,10 @@ export class Reporter {
355
356
  const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
356
357
  const latest = Array.from(this.pendingTests.values()).pop();
357
358
  if (latest) {
358
- latest.errorMsg = err;
359
+ latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
359
360
  }
360
361
  else {
361
- console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${err}\x1b[0m`);
362
+ this.pendingErrorMsg = this.pendingErrorMsg ? `${this.pendingErrorMsg}\n${err}` : err;
362
363
  }
363
364
  return;
364
365
  }
@@ -394,16 +395,14 @@ export class Reporter {
394
395
  this.failedCount++;
395
396
  const testCase = this.pendingTests.get(title) || this.resolveTestCase(title);
396
397
  const steps = testCase?.steps || [];
397
- const errorMsg = testCase?.errorMsg;
398
+ const errorMsg = testCase?.errorMsg || this.pendingErrorMsg;
399
+ this.pendingErrorMsg = undefined;
398
400
  const workerId = testCase?.workerId;
399
401
  this.pendingTests.delete(title);
400
402
  if (workerId !== undefined) {
401
403
  this.workerToTest.delete(workerId);
402
404
  }
403
405
  this.clearActiveSlots();
404
- if (errorMsg) {
405
- console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errorMsg}\x1b[0m`);
406
- }
407
406
  console.log(`${chalk.hex('#f87171')('✗')} ${chalk.white.bold(title)} ${chalk.gray(`(${duration}ms)`)}`);
408
407
  for (const step of steps) {
409
408
  if (step.passed) {
@@ -413,6 +412,13 @@ export class Reporter {
413
412
  console.log(` ${chalk.red('✖')} ${chalk.red(step.text)} ${chalk.gray(`(${step.duration}ms)`)}`);
414
413
  }
415
414
  }
415
+ if (errorMsg) {
416
+ const errLines = errorMsg.split('\n');
417
+ console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
418
+ for (let i = 1; i < errLines.length; i++) {
419
+ console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
420
+ }
421
+ }
416
422
  console.log('');
417
423
  this.renderActiveSlots();
418
424
  return;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import http from 'node:http';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { DevServerManager } from '../dev-server-manager.js';
4
+ describe('DevServerManager', () => {
5
+ it('parses command strings into binary and argv array without shell', () => {
6
+ expect(DevServerManager.parseCommand('pnpm --filter sso dev')).toEqual({
7
+ bin: 'pnpm',
8
+ args: ['--filter', 'sso', 'dev'],
9
+ });
10
+ expect(DevServerManager.parseCommand('nx run auth-e2e:serve --port=5174')).toEqual({
11
+ bin: 'nx',
12
+ args: ['run', 'auth-e2e:serve', '--port=5174'],
13
+ });
14
+ expect(DevServerManager.parseCommand('node "./scripts/serve.js" --mode "dev test"')).toEqual({
15
+ bin: 'node',
16
+ args: ['./scripts/serve.js', '--mode', 'dev test'],
17
+ });
18
+ });
19
+ it('probes listening HTTP server and detects readiness', async () => {
20
+ const server = http.createServer((_req, res) => {
21
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
22
+ res.end('OK');
23
+ });
24
+ await new Promise((resolve) => {
25
+ server.listen(0, '127.0.0.1', () => resolve());
26
+ });
27
+ const addr = server.address();
28
+ const url = `http://127.0.0.1:${addr.port}`;
29
+ const isReady = await DevServerManager.probeUrl(url, 1000);
30
+ expect(isReady).toBe(true);
31
+ const isNonExistentReady = await DevServerManager.probeUrl('http://127.0.0.1:49999', 200);
32
+ expect(isNonExistentReady).toBe(false);
33
+ await new Promise((resolve) => server.close(() => resolve()));
34
+ });
35
+ it('reuses existing running server without spawning new process', async () => {
36
+ const server = http.createServer((_req, res) => {
37
+ res.writeHead(200);
38
+ res.end('OK');
39
+ });
40
+ await new Promise((resolve) => {
41
+ server.listen(0, '127.0.0.1', () => resolve());
42
+ });
43
+ const addr = server.address();
44
+ const url = `http://127.0.0.1:${addr.port}`;
45
+ const manager = new DevServerManager();
46
+ const result = await manager.ensureServer({
47
+ name: 'test-server',
48
+ command: 'echo "should not run"',
49
+ url,
50
+ reuseExistingServer: true,
51
+ });
52
+ expect(result.reused).toBe(true);
53
+ expect(result.name).toBe('test-server');
54
+ expect(result.url).toBe(url);
55
+ await manager.stopAll();
56
+ await new Promise((resolve) => server.close(() => resolve()));
57
+ });
58
+ it('filters dev servers by target host filter', async () => {
59
+ const manager = new DevServerManager();
60
+ const spy = vi.spyOn(manager, 'ensureServer').mockResolvedValue({
61
+ name: 'sso',
62
+ url: 'https://sso.local:5174',
63
+ reused: true,
64
+ });
65
+ await manager.startAll([
66
+ { name: 'sso', command: 'pnpm sso:dev', url: 'https://sso.local:5174' },
67
+ { name: 'fam', command: 'pnpm fam:dev', url: 'https://fam.local:5175' },
68
+ ], 'sso');
69
+ expect(spy).toHaveBeenCalledTimes(1);
70
+ expect(spy).toHaveBeenCalledWith(expect.objectContaining({ name: 'sso' }), expect.any(String));
71
+ });
72
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import fs from 'fs';
2
+ import { execFileSync } from 'child_process';
3
+ const cachePath = process.argv[2];
4
+ if (!cachePath)
5
+ process.exit(0);
6
+ const PACKAGES = ['@testspectra/cli', '@testspectra/matchers'];
7
+ function getLatestVersion(pkg) {
8
+ try {
9
+ const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
10
+ stdio: ['ignore', 'pipe', 'ignore'],
11
+ timeout: 10000,
12
+ })
13
+ .toString()
14
+ .trim()
15
+ .replace(/"/g, '');
16
+ return out || null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ async function run() {
23
+ const updates = [];
24
+ for (const pkg of PACKAGES) {
25
+ const latest = getLatestVersion(pkg);
26
+ if (latest) {
27
+ // Hardcode a check or let the main thread do the actual diff
28
+ // For now, let's just record the latest versions.
29
+ updates.push({ pkg, latest });
30
+ }
31
+ }
32
+ // To properly do a diff, we should read package.json of the project.
33
+ // Since we don't have cwd here, we just save the latest versions and let the main thread diff it,
34
+ // OR we can pass the current versions as arguments to this script.
35
+ // For simplicity, we just save the latest versions.
36
+ const cacheData = {
37
+ timestamp: Date.now(),
38
+ latestVersions: updates,
39
+ updatesAvailable: updates, // Main thread will filter this properly later or we just mock it for architecture
40
+ };
41
+ fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
42
+ }
43
+ run().catch(() => { });
@@ -0,0 +1,15 @@
1
+ export interface DependencyUpdateStatus {
2
+ name: string;
3
+ kind: 'package' | 'driver';
4
+ identifier: string;
5
+ current: string | null;
6
+ latest: string | null;
7
+ needsUpdate: boolean;
8
+ }
9
+ /**
10
+ * Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
11
+ * Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
12
+ * attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
13
+ * private registry unavailable) — the row still exposes its reinstall action.
14
+ */
15
+ export declare function checkDriverUpdates(): Promise<DependencyUpdateStatus[]>;
@@ -0,0 +1,107 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { runSystemChecks } from '../commands/doctor.js';
3
+ const CHROME_FOR_TESTING_API = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json';
4
+ const BUN_RELEASES_API = 'https://api.github.com/repos/oven-sh/bun/releases/latest';
5
+ const PLATFORM_TOOLS_XML = 'https://dl.google.com/android/repository/repository2-3.xml';
6
+ function normalizeVersion(raw) {
7
+ if (!raw)
8
+ return null;
9
+ const match = raw.match(/(\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?)/);
10
+ return match ? match[1] : null;
11
+ }
12
+ async function fetchJson(url) {
13
+ try {
14
+ const res = await fetch(url, {
15
+ signal: AbortSignal.timeout(8000),
16
+ headers: { 'User-Agent': 'testspectra-cli' },
17
+ });
18
+ if (!res.ok)
19
+ return null;
20
+ return await res.json();
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function latestChromeVersion() {
27
+ const json = await fetchJson(CHROME_FOR_TESTING_API);
28
+ return json?.channels?.Stable?.version ?? null;
29
+ }
30
+ async function latestBunVersion() {
31
+ const json = await fetchJson(BUN_RELEASES_API);
32
+ const tag = json?.tag_name;
33
+ return typeof tag === 'string' ? tag.replace(/^bun-v/, '') : null;
34
+ }
35
+ async function latestAdbVersion() {
36
+ try {
37
+ const res = await fetch(PLATFORM_TOOLS_XML, {
38
+ signal: AbortSignal.timeout(8000),
39
+ headers: { 'User-Agent': 'testspectra-cli' },
40
+ });
41
+ if (!res.ok)
42
+ return null;
43
+ const xml = await res.text();
44
+ const block = xml.match(/<remotePackage[^>]*path="platform-tools"[\s\S]*?<\/remotePackage>/);
45
+ if (!block)
46
+ return null;
47
+ const rev = block[0].match(/<revision>[\s\S]*?<major>(\d+)<\/major>[\s\S]*?<minor>(\d+)<\/minor>[\s\S]*?<micro>(\d+)<\/micro>/);
48
+ return rev ? `${rev[1]}.${rev[2]}.${rev[3]}` : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function latestAndroidAgentVersion() {
55
+ try {
56
+ return (execFileSync('npm', ['view', '@testspectra/android-agent', 'version', '--json'], {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ timeout: 10000,
59
+ })
60
+ .toString()
61
+ .trim()
62
+ .replace(/"/g, '') || null);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Best-effort "installed vs latest" report for the managed system drivers and the Spectra Android
70
+ * Agent, keyed by the exact `name` `spectra doctor` reports so the VS Code Preflight panel can
71
+ * attach a per-row update badge. `latest` stays `null` when it cannot be determined (offline,
72
+ * private registry unavailable) — the row still exposes its reinstall action.
73
+ */
74
+ export async function checkDriverUpdates() {
75
+ let installed;
76
+ try {
77
+ const result = await runSystemChecks();
78
+ installed = result.dependencies;
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ const [chrome, bun, adb] = await Promise.all([latestChromeVersion(), latestBunVersion(), latestAdbVersion()]);
84
+ const agent = latestAndroidAgentVersion();
85
+ const candidates = [
86
+ { name: 'Google Chrome', latest: chrome },
87
+ { name: 'Bun', latest: bun },
88
+ { name: 'ADB (Android Debug Bridge)', latest: adb },
89
+ { name: 'Spectra Android Agent', latest: agent },
90
+ ];
91
+ const updates = [];
92
+ for (const { name, latest } of candidates) {
93
+ const dep = installed.find((d) => d.name === name);
94
+ if (!dep?.installed)
95
+ continue;
96
+ const current = normalizeVersion(dep.version);
97
+ updates.push({
98
+ name,
99
+ kind: 'driver',
100
+ identifier: name,
101
+ current,
102
+ latest,
103
+ needsUpdate: !!latest && !!current && current !== latest,
104
+ });
105
+ }
106
+ return updates;
107
+ }
@@ -0,0 +1,40 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { DevServerConfig } from '../config/schema.js';
3
+ export interface RunningDevServer {
4
+ name: string;
5
+ url: string;
6
+ process?: ChildProcess;
7
+ reused: boolean;
8
+ }
9
+ export declare class DevServerManager {
10
+ private spawnedProcesses;
11
+ private cleanupRegistered;
12
+ constructor();
13
+ /**
14
+ * Safely splits a shell command string into binary executable and argument array without shell expansion.
15
+ */
16
+ static parseCommand(command: string): {
17
+ bin: string;
18
+ args: string[];
19
+ };
20
+ /**
21
+ * Probes a URL to verify if the server is actively listening and responsive.
22
+ * Performs a rapid TCP connection test followed by an HTTP/HTTPS request.
23
+ */
24
+ static probeUrl(targetUrl: string, timeoutMs?: number): Promise<boolean>;
25
+ /**
26
+ * Ensures an individual dev server is running. If already responding and reuseExistingServer !== false,
27
+ * reuses the instance. Otherwise, spawns the process and polls until ready.
28
+ */
29
+ ensureServer(config: DevServerConfig, rootCwd?: string): Promise<RunningDevServer>;
30
+ /**
31
+ * Starts all matching dev servers in parallel.
32
+ */
33
+ startAll(configs?: DevServerConfig[], targetHostFilter?: string, rootCwd?: string): Promise<RunningDevServer[]>;
34
+ /**
35
+ * Safely terminates all spawned dev server child processes.
36
+ */
37
+ stopAll(): Promise<void>;
38
+ private killProcess;
39
+ private registerExitHooks;
40
+ }