@testspectra/cli 1.1.8-rc.24 → 1.1.8-rc.28

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 (57) hide show
  1. package/README.md +11 -8
  2. package/bin/.testspectra-runner.hash +1 -0
  3. package/dist/commands/__tests__/doctor-filter.test.js +37 -0
  4. package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
  5. package/dist/commands/__tests__/doctor-scope.test.js +24 -0
  6. package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
  7. package/dist/commands/__tests__/test-error-streaming.test.js +52 -0
  8. package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
  9. package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
  10. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  11. package/dist/commands/__tests__/upgrade.test.js +95 -0
  12. package/dist/commands/add.js +3 -3
  13. package/dist/commands/doctor.d.ts +9 -1
  14. package/dist/commands/doctor.js +16 -3
  15. package/dist/commands/install.js +3 -0
  16. package/dist/commands/test.d.ts +13 -0
  17. package/dist/commands/test.js +114 -22
  18. package/dist/commands/upgrade.d.ts +8 -1
  19. package/dist/commands/upgrade.js +63 -13
  20. package/dist/generator/architecture-doc-generator.js +1 -3
  21. package/dist/index.js +30 -15
  22. package/dist/linter/discovery.js +2 -2
  23. package/dist/linter/schemas/spec.schema.d.ts +6 -6
  24. package/dist/runner/bridge.d.ts +1 -1
  25. package/dist/runner/bridge.js +45 -16
  26. package/dist/runner/reporter.js +11 -3
  27. package/dist/utils/background-update-check.js +1 -8
  28. package/dist/utils/dependency-updates.d.ts +15 -0
  29. package/dist/utils/dependency-updates.js +107 -0
  30. package/package.json +8 -9
  31. package/templates/default/package.json +2 -2
  32. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  33. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  34. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  35. package/templates/nx/.nx/workspace-data/d/daemon.log +983 -6269
  36. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  37. package/templates/nx/.nx/workspace-data/file-map.json +20 -34
  38. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  39. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  40. package/templates/nx/.nx/workspace-data/project-graph.json +3 -1
  41. package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
  42. package/templates/nx/package.json +2 -2
  43. package/templates/react-component/package.json +3 -3
  44. package/templates/react-component/spectra.config.ts +0 -10
  45. package/dist/.tsbuildinfo +0 -1
  46. package/dist/commands/update.d.ts +0 -3
  47. package/dist/commands/update.js +0 -109
  48. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
  49. package/dist/lifecycle/hook-discovery.d.ts +0 -24
  50. package/dist/lifecycle/hook-discovery.js +0 -60
  51. package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
  52. package/dist/lifecycle/hook-dispatcher.js +0 -190
  53. package/dist/lifecycle/index.d.ts +0 -3
  54. package/dist/lifecycle/index.js +0 -3
  55. package/dist/lifecycle/parallel-grouping.d.ts +0 -27
  56. package/dist/lifecycle/parallel-grouping.js +0 -151
  57. /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
@@ -3,9 +3,21 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { RustCoreBridge } from '../runner/bridge.js';
6
+ import { checkDriverUpdates } from '../utils/dependency-updates.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  const PACKAGES = ['@testspectra/cli', '@testspectra/matchers', '@testspectra/react'];
10
+ /** Filters requested package names against the known catalog; empty means "all". */
11
+ function selectPackages(requested) {
12
+ if (requested.length === 0)
13
+ return [...PACKAGES];
14
+ const known = new Set(PACKAGES);
15
+ const unknown = requested.filter((p) => !known.has(p));
16
+ if (unknown.length > 0) {
17
+ console.warn(`\x1b[33mIgnoring unknown TestSpectra package(s): ${unknown.join(', ')}\x1b[0m`);
18
+ }
19
+ return requested.filter((p) => known.has(p));
20
+ }
9
21
  function getLatestVersion(pkg) {
10
22
  try {
11
23
  const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
@@ -43,11 +55,27 @@ function detectPackageManager(cwd) {
43
55
  return 'bun';
44
56
  return 'npm';
45
57
  }
46
- function buildInstallCmd(pm, pkgSpecs) {
58
+ function collectPackageUpdates(cwd, packages) {
59
+ return packages.map((pkg) => {
60
+ const current = getCurrentVersion(pkg, cwd);
61
+ const latest = getLatestVersion(pkg);
62
+ return { pkg, current, latest, needsUpdate: !!latest && latest !== current };
63
+ });
64
+ }
65
+ function printUpdateRow(label, current, latest, needsUpdate) {
66
+ const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
67
+ const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[90m(unknown)\x1b[0m';
68
+ const arrow = needsUpdate ? ` → ${latestStr}` : '';
69
+ const status = needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
70
+ console.log(`\x1b[36m│\x1b[0m ${status} ${label.padEnd(28)} ${currentStr}${arrow}`);
71
+ }
72
+ function buildInstallCmd(pm, pkgSpecs, cwd) {
47
73
  const list = pkgSpecs.join(' ');
48
74
  switch (pm) {
49
- case 'pnpm':
50
- return `pnpm add -D ${list}`;
75
+ case 'pnpm': {
76
+ const isWorkspaceRoot = fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml'));
77
+ return isWorkspaceRoot ? `pnpm add -D -w ${list}` : `pnpm add -D ${list}`;
78
+ }
51
79
  case 'bun':
52
80
  return `bun add -d ${list}`;
53
81
  case 'npm':
@@ -55,9 +83,24 @@ function buildInstallCmd(pm, pkgSpecs) {
55
83
  return `npm install -D ${list}`;
56
84
  }
57
85
  }
58
- export async function upgradeCommand(options = {}) {
86
+ export async function upgradeCommand(requestedPackages = [], options = {}) {
59
87
  const cwd = process.cwd();
60
88
  const pm = detectPackageManager(cwd);
89
+ const selected = selectPackages(requestedPackages);
90
+ const targeted = requestedPackages.length > 0;
91
+ if (targeted && selected.length === 0) {
92
+ console.error(`\x1b[31mNo known TestSpectra package matched: ${requestedPackages.join(', ')}. Known packages: ${PACKAGES.join(', ')}\x1b[0m`);
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ // Machine-readable mode for the VS Code Preflight panel: implies `--check`, never mutates.
97
+ if (options.json) {
98
+ const packages = collectPackageUpdates(cwd, selected);
99
+ const drivers = targeted ? [] : await checkDriverUpdates();
100
+ const updatesAvailable = [...packages, ...drivers].filter((u) => u.needsUpdate).length;
101
+ console.log(JSON.stringify({ packages, drivers, updatesAvailable }, null, 2));
102
+ return;
103
+ }
61
104
  console.log('\x1b[36m┌ 🚀 TestSpectra Self-Upgrade Mechanism\x1b[0m');
62
105
  console.log('\x1b[36m│\x1b[0m');
63
106
  const updates = [];
@@ -66,11 +109,8 @@ export async function upgradeCommand(options = {}) {
66
109
  if (!options.driversOnly) {
67
110
  console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 1: Workspace Packages\x1b[0m (Manager: \x1b[33m${pm}\x1b[0m)`);
68
111
  console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
69
- for (const pkg of PACKAGES) {
70
- const current = getCurrentVersion(pkg, cwd);
71
- const latest = getLatestVersion(pkg);
72
- const needsUpdate = !!latest && latest !== current;
73
- updates.push({ pkg, current, latest, needsUpdate });
112
+ updates.push(...collectPackageUpdates(cwd, selected));
113
+ for (const { pkg, current, latest, needsUpdate } of updates) {
74
114
  const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
75
115
  const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
76
116
  const arrow = needsUpdate ? ` → ${latestStr}` : '';
@@ -84,7 +124,7 @@ export async function upgradeCommand(options = {}) {
84
124
  }
85
125
  else if (toUpdate.length > 0) {
86
126
  const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
87
- const installCmd = buildInstallCmd(pm, pkgSpecs);
127
+ const installCmd = buildInstallCmd(pm, pkgSpecs, cwd);
88
128
  console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
89
129
  try {
90
130
  execSync(installCmd, { cwd, stdio: 'inherit' });
@@ -101,11 +141,21 @@ export async function upgradeCommand(options = {}) {
101
141
  console.log('\x1b[36m│\x1b[0m');
102
142
  }
103
143
  if (options.check) {
144
+ if (!targeted) {
145
+ const drivers = await checkDriverUpdates();
146
+ if (drivers.length > 0) {
147
+ console.log('\x1b[36m│\x1b[0m \x1b[1mDriver & Agent Status\x1b[0m');
148
+ for (const dep of drivers) {
149
+ printUpdateRow(dep.name, dep.current, dep.latest, dep.needsUpdate);
150
+ }
151
+ console.log('\x1b[36m│\x1b[0m');
152
+ }
153
+ }
104
154
  console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
105
155
  return;
106
156
  }
107
157
  // Phase 2: Drivers and Binaries
108
- if (!options.packagesOnly) {
158
+ if (!options.packagesOnly && !targeted) {
109
159
  console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 2: System Binaries & Drivers\x1b[0m`);
110
160
  try {
111
161
  const binPath = RustCoreBridge.resolveBinaryPath();
@@ -120,8 +170,8 @@ export async function upgradeCommand(options = {}) {
120
170
  env: {
121
171
  ...process.env,
122
172
  TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
123
- TEST_SPECTRA_FORCE_LATEST: '1'
124
- }
173
+ TEST_SPECTRA_FORCE_LATEST: '1',
174
+ },
125
175
  });
126
176
  console.log('\x1b[36m│\x1b[0m \x1b[32m✓ Drivers updated successfully.\x1b[0m');
127
177
  }
@@ -64,9 +64,7 @@ export function generateArchitectureDoc(cwd) {
64
64
  }
65
65
  const sharedScope = scopes.find((s) => s.isShared);
66
66
  const featureE2eDirs = scopes.filter((s) => !s.isShared).map((s) => s.dir);
67
- const sharedTestingRelPath = sharedScope
68
- ? path.relative(cwd, sharedScope.dir).replace(/\\/g, '/')
69
- : 'shared/testing';
67
+ const sharedTestingRelPath = sharedScope ? path.relative(cwd, sharedScope.dir).replace(/\\/g, '/') : 'shared/testing';
70
68
  const e2eFolderName = featureE2eDirs.length > 0 ? path.basename(featureE2eDirs[0]) : 'e2e';
71
69
  const content = renderArchitectureDoc(path.join(docsTemplateDir, 'ARCHITECTURE.nx.md'), {
72
70
  __SHARED_TESTING_REL_PATH__: sharedTestingRelPath,
package/dist/index.js CHANGED
@@ -25,10 +25,10 @@ export * from './types/generator.js';
25
25
  export * from '@testspectra/matchers';
26
26
  export function createCliProgram() {
27
27
  const program = new Command();
28
- program.name('spectra').description('TestSpectra Zero-Config Cross-Platform Test Runner CLI').version('1.0.0');
28
+ program.name('spectra').description('TestSpectra Cross-Platform Test Runner CLI').version('1.0.0');
29
29
  program
30
30
  .command('init')
31
- .description('Initialize testspectra.config.json and project directories')
31
+ .description('Initialize spectra.config.ts and project directories')
32
32
  .option('-t, --template <template>', 'Project template: default, nx, react-component')
33
33
  .option('-f, --force', 'Overwrite existing configuration if present')
34
34
  .action(initCommand);
@@ -41,15 +41,16 @@ export function createCliProgram() {
41
41
  program
42
42
  .command('install [dependency]')
43
43
  .description('Install missing testing dependencies, browser drivers, and workspace packages')
44
- .option('-f, --force', 'Force re-installation of dependencies')
44
+ .option('-f, --force', 'Remove existing artifacts and perform a clean reinstall of the dependency')
45
45
  .addHelpText('after', `
46
46
  Examples:
47
47
  $ spectra install Install all missing required dependencies automatically
48
48
  $ spectra install chrome Install Google Chrome for Testing & matching Chromedriver
49
49
  $ spectra install adb Install Google Android SDK Platform Tools (ADB)
50
50
  $ spectra install bun Install Bun runtime
51
- $ spectra install android-driver Install Spectra Android Native Driver & WDIO Service
52
- $ spectra install wdio Install WebDriverIO core packages and matchers`)
51
+ $ spectra install android-agent Install Spectra Android Native Agent (spectra-agent.jar)
52
+ $ spectra install android-agent -f Clean reinstall Spectra Android Native Agent
53
+ $ spectra install packages Install TestSpectra CLI, Matchers & Android Agent packages`)
53
54
  .action(installCommand);
54
55
  program
55
56
  .command('demo')
@@ -86,23 +87,31 @@ Examples:
86
87
  .action(lintCommand);
87
88
  program
88
89
  .command('doctor')
89
- .description('Verify local environment prerequisites (ADB, Spectra Android Driver, Chrome, Bun)')
90
+ .description('Verify local environment prerequisites (Bun, Git, ADB, Chrome, Spectra Android Agent, workspace packages)')
90
91
  .option('-v, --verbose', 'Show full installation paths for all detected runtimes and packages')
92
+ .option('--scope <category>', 'Only check one category: core, web, mobile (default: all)')
93
+ .option('--only <keys>', 'Only check specific dependencies (comma-separated): bun, git, cli, chrome, matchers, adb, agent')
91
94
  .option('--fix', 'Attempt automatic fix / download of missing tools')
92
95
  .option('--json', 'Output diagnostic results as JSON')
93
96
  .action(doctorCommand);
94
97
  program
95
- .command('upgrade')
98
+ .command('upgrade [packages...]')
96
99
  .alias('update')
97
100
  .description('Upgrade TestSpectra ecosystem packages and system drivers to the latest versions')
98
101
  .option('--check', 'Only check for available updates without installing')
99
102
  .option('--packages-only', 'Only upgrade NPM packages in package.json')
100
103
  .option('--drivers-only', 'Only upgrade system binaries and drivers (ADB, Agent, Chrome)')
104
+ .option('--json', 'Output the update check (packages + drivers) as JSON (implies --check, no changes applied)')
105
+ .addHelpText('after', `
106
+ Examples:
107
+ $ spectra upgrade Upgrade all packages and system drivers
108
+ $ spectra upgrade @testspectra/cli Upgrade only the CLI package
109
+ $ spectra upgrade @testspectra/cli @testspectra/matchers Upgrade specific packages
110
+ $ spectra upgrade --check Check what is outdated (packages + drivers)`)
101
111
  .action(upgradeCommand);
102
112
  program
103
113
  .command('devices')
104
- .description('List connected Android/iOS devices and local browsers')
105
- .option('-t, --target <target>', 'Filter scope: android, ios, web, all', 'all')
114
+ .description('List connected Android devices (ADB)')
106
115
  .option('--json', 'Output results as JSON')
107
116
  .action(devicesCommand);
108
117
  program
@@ -180,23 +189,29 @@ Examples:
180
189
  .action(runCommand);
181
190
  program
182
191
  .command('test')
183
- .description('Run React component tests via @testspectra/react (bundled Vitest browser mode)')
192
+ .description('Run React component tests via @testspectra/react')
193
+ .option('-r, --runner <engine>', 'Component test runner engine: spectra (default, native CDP) or vitest (Playwright browser mode)', 'spectra')
184
194
  .option('-w, --cwd <dir>', 'Custom workspace directory')
185
195
  .option('-s, --spec <path>', 'Spec file path (or substring filter) to execute')
186
196
  .option('--headless', 'Run in headless mode (default)')
187
197
  .option('--no-headless', 'Run in headed mode')
188
198
  .option('--headed', 'Run the browser in visible GUI mode (alias for --no-headless)')
189
- .option('--step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
199
+ .option('-d, --step-delay <ms>', 'Delay after each action/assertion in milliseconds (slow motion, for observing DOM transitions in --headed mode)')
190
200
  .option('--delay <ms>', 'Alias for --step-delay')
191
- .option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default). Browser mode has no numeric N-worker sizing like `spectra run`.')
192
- .option('-o, --output <path>', 'Write a Vitest JSON test report to this path')
201
+ .option('-c, --concurrency <number>', 'Run spec files sequentially (1) or in parallel (>1, default).')
202
+ .option('-o, --output <path>', 'Write a JSON test report to this path')
193
203
  .action(testCommand);
194
204
  return program;
195
205
  }
196
206
  export async function runCli(args = process.argv) {
197
- const { checkUpdateInBackground, displayUpdateNotificationIfNeeded } = await import('./utils/update-notifier.js');
198
- checkUpdateInBackground();
199
207
  const program = createCliProgram();
200
208
  await program.parseAsync(args);
209
+ // Commands whose stdout must stay machine-readable or is purely diagnostic never emit the
210
+ // interactive "update available" banner.
211
+ const silentCommands = ['doctor', 'install', 'upgrade', 'update'];
212
+ if (silentCommands.includes(program.args[0]))
213
+ return;
214
+ const { checkUpdateInBackground, displayUpdateNotificationIfNeeded } = await import('./utils/update-notifier.js');
215
+ checkUpdateInBackground();
201
216
  displayUpdateNotificationIfNeeded();
202
217
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { ConfigLoader } from '../config/loader.js';
3
4
  /**
4
5
  * Scans a workspace directory for all `project.json` files that have the
5
6
  * "testspectra:shared" tag, and returns their parent folder paths.
@@ -96,8 +97,7 @@ export function isInsideSharedLibrary(filePath, workspaceDir) {
96
97
  export function findProjectRoot(startDir) {
97
98
  let current = path.resolve(startDir);
98
99
  while (true) {
99
- if (fs.existsSync(path.join(current, 'spectra.config.ts')) ||
100
- fs.existsSync(path.join(current, 'spectra.config.js'))) {
100
+ if (ConfigLoader.CONFIG_FILE_NAMES.some((name) => fs.existsSync(path.join(current, name)))) {
101
101
  return current;
102
102
  }
103
103
  const parent = path.dirname(current);
@@ -11,11 +11,11 @@ export declare const CoverageSchema: z.ZodObject<{
11
11
  web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
12
12
  mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
13
13
  }, "strip", z.ZodTypeAny, {
14
- web?: "automated" | "manual" | "unsupported" | undefined;
15
14
  mobile?: "automated" | "manual" | "unsupported" | undefined;
16
- }, {
17
15
  web?: "automated" | "manual" | "unsupported" | undefined;
16
+ }, {
18
17
  mobile?: "automated" | "manual" | "unsupported" | undefined;
18
+ web?: "automated" | "manual" | "unsupported" | undefined;
19
19
  }>;
20
20
  export declare const SpecFrontmatterSchema: z.ZodObject<{
21
21
  id: z.ZodString;
@@ -27,11 +27,11 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
27
27
  web: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
28
28
  mobile: z.ZodOptional<z.ZodEnum<["automated", "manual", "unsupported"]>>;
29
29
  }, "strip", z.ZodTypeAny, {
30
- web?: "automated" | "manual" | "unsupported" | undefined;
31
30
  mobile?: "automated" | "manual" | "unsupported" | undefined;
32
- }, {
33
31
  web?: "automated" | "manual" | "unsupported" | undefined;
32
+ }, {
34
33
  mobile?: "automated" | "manual" | "unsupported" | undefined;
34
+ web?: "automated" | "manual" | "unsupported" | undefined;
35
35
  }>>;
36
36
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
37
37
  }, "strip", z.ZodTypeAny, {
@@ -41,8 +41,8 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
41
41
  priority: "Critical" | "High" | "Medium" | "Low";
42
42
  caseType: "Positive" | "Negative" | "Edge";
43
43
  coverage?: {
44
- web?: "automated" | "manual" | "unsupported" | undefined;
45
44
  mobile?: "automated" | "manual" | "unsupported" | undefined;
45
+ web?: "automated" | "manual" | "unsupported" | undefined;
46
46
  } | undefined;
47
47
  tags?: string[] | undefined;
48
48
  }, {
@@ -52,8 +52,8 @@ export declare const SpecFrontmatterSchema: z.ZodObject<{
52
52
  priority: "Critical" | "High" | "Medium" | "Low";
53
53
  caseType: "Positive" | "Negative" | "Edge";
54
54
  coverage?: {
55
- web?: "automated" | "manual" | "unsupported" | undefined;
56
55
  mobile?: "automated" | "manual" | "unsupported" | undefined;
56
+ web?: "automated" | "manual" | "unsupported" | undefined;
57
57
  } | undefined;
58
58
  tags?: string[] | undefined;
59
59
  }>;
@@ -3,7 +3,7 @@ import { Reporter, TestRunResult } from './reporter.js';
3
3
  export interface RunOptions {
4
4
  baseDir: string;
5
5
  appDataPath: string;
6
- platform: 'web' | 'android' | 'ios' | 'common' | 'mobile';
6
+ platform: 'web' | 'android' | 'ios' | 'common' | 'mobile' | 'component';
7
7
  suite?: string;
8
8
  testCases?: Array<{
9
9
  id: string;
@@ -16,16 +16,42 @@ 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
+ const workspaceRoot = path.resolve(__dirname, '../../..');
33
+ const isMonorepo = fs.existsSync(path.join(workspaceRoot, 'core/Cargo.toml'));
34
+ const candidatePaths = [
35
+ path.join(workspaceRoot, 'core/target/release', binaryName),
36
+ path.join(workspaceRoot, 'target/release', binaryName),
37
+ path.join(workspaceRoot, 'cli/bin', binaryName),
38
+ path.join(__dirname, '../../bin', binaryName),
39
+ path.join(workspaceRoot, 'core/cli/target/release', binaryName),
40
+ path.join(workspaceRoot, 'core/target/debug', binaryName),
41
+ path.join(workspaceRoot, 'target/debug', binaryName),
42
+ path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
43
+ path.join(__dirname, '../../bin/testspectra-runner'),
44
+ ];
45
+ if (isMonorepo) {
46
+ for (const p of candidatePaths) {
47
+ if (fs.existsSync(p)) {
48
+ return p;
49
+ }
50
+ if (fs.existsSync(`${p}.exe`)) {
51
+ return `${p}.exe`;
52
+ }
53
+ }
54
+ }
29
55
  // 1. Production install: resolve the prebuilt binary from the platform-specific
30
56
  // optionalDependency package (e.g. @testspectra/cli-darwin-arm64). This is how the
31
57
  // published npm package ships native binaries for every supported platform without
@@ -52,20 +78,7 @@ export class RustCoreBridge {
52
78
  // Optional dependency not installed — fall through to the workspace dev paths below.
53
79
  }
54
80
  }
55
- // 2. Monorepo dev fallback: look for a precompiled release/debug binary in the workspace
56
- // target, as produced by scripts/ensure-runner-binary.js during local development.
57
- const workspaceRoot = path.resolve(__dirname, '../../..');
58
- const candidatePaths = [
59
- path.join(workspaceRoot, 'core/target/release', binaryName),
60
- path.join(workspaceRoot, 'target/release', binaryName),
61
- path.join(workspaceRoot, 'cli/bin', binaryName),
62
- path.join(__dirname, '../../bin', binaryName),
63
- path.join(workspaceRoot, 'core/cli/target/release', binaryName),
64
- path.join(workspaceRoot, 'core/target/debug', binaryName),
65
- path.join(workspaceRoot, 'target/debug', binaryName),
66
- path.join(workspaceRoot, 'core/cli/target/debug', binaryName),
67
- path.join(__dirname, '../../bin/testspectra-runner'),
68
- ];
81
+ // 2. Dev fallback if not already resolved above
69
82
  for (const p of candidatePaths) {
70
83
  if (fs.existsSync(p)) {
71
84
  return p;
@@ -118,6 +131,8 @@ export class RustCoreBridge {
118
131
  });
119
132
  let runStatus = 'failed';
120
133
  let runDuration = '0s';
134
+ let completePassedCount;
135
+ let completeFailedCount;
121
136
  child.stdout.on('data', (data) => {
122
137
  const lines = data.toString().split('\n');
123
138
  for (const line of lines) {
@@ -160,6 +175,12 @@ export class RustCoreBridge {
160
175
  else if (parsed.type === 'complete') {
161
176
  runStatus = parsed.status;
162
177
  runDuration = parsed.duration;
178
+ if (typeof parsed.passed_count === 'number') {
179
+ completePassedCount = parsed.passed_count;
180
+ }
181
+ if (typeof parsed.failed_count === 'number') {
182
+ completeFailedCount = parsed.failed_count;
183
+ }
163
184
  }
164
185
  else if (parsed.type === 'error') {
165
186
  reporter.addLog({
@@ -195,6 +216,14 @@ export class RustCoreBridge {
195
216
  });
196
217
  child.on('close', (code) => {
197
218
  let result = reporter.generateResult(code === 0 ? 'passed' : runStatus, runDuration);
219
+ if (result.passedCount === 0 && result.failedCount === 0) {
220
+ if (completePassedCount !== undefined) {
221
+ result.passedCount = completePassedCount;
222
+ }
223
+ if (completeFailedCount !== undefined) {
224
+ result.failedCount = completeFailedCount;
225
+ }
226
+ }
198
227
  // Only trust a pre-existing outputJsonPath when it was actually written during THIS run
199
228
  // (mtime at/after runStartedAt) — NOT just when the process exited with code 0. The
200
229
  // native runner writes its own complete report once the pipeline reaches the report
@@ -355,10 +355,14 @@ export class Reporter {
355
355
  const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
356
356
  const latest = Array.from(this.pendingTests.values()).pop();
357
357
  if (latest) {
358
- latest.errorMsg = err;
358
+ latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
359
359
  }
360
360
  else {
361
- console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${err}\x1b[0m`);
361
+ const errLines = err.split('\n');
362
+ console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
363
+ for (let i = 1; i < errLines.length; i++) {
364
+ console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
365
+ }
362
366
  }
363
367
  return;
364
368
  }
@@ -402,7 +406,11 @@ export class Reporter {
402
406
  }
403
407
  this.clearActiveSlots();
404
408
  if (errorMsg) {
405
- console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errorMsg}\x1b[0m`);
409
+ const errLines = errorMsg.split('\n');
410
+ console.log(` \x1b[38;2;251;191;36m↳ Error:\x1b[0m \x1b[38;2;248;113;113m${errLines[0]}\x1b[0m`);
411
+ for (let i = 1; i < errLines.length; i++) {
412
+ console.log(` \x1b[38;2;248;113;113m${errLines[i]}\x1b[0m`);
413
+ }
406
414
  }
407
415
  console.log(`${chalk.hex('#f87171')('✗')} ${chalk.white.bold(title)} ${chalk.gray(`(${duration}ms)`)}`);
408
416
  for (const step of steps) {
@@ -19,13 +19,6 @@ function getLatestVersion(pkg) {
19
19
  return null;
20
20
  }
21
21
  }
22
- function getCurrentVersion(pkg) {
23
- // In a real scenario we'd check the workspace.
24
- // For background check, we can check global or try to find it via npm ls,
25
- // but simpler to just pull from the currently running CLI package.json for '@testspectra/cli'
26
- // For background script it's tricky to know the workspace, so we just check if it's available.
27
- return null; // A robust implementation would pass cwd to the background script
28
- }
29
22
  async function run() {
30
23
  const updates = [];
31
24
  for (const pkg of PACKAGES) {
@@ -43,7 +36,7 @@ async function run() {
43
36
  const cacheData = {
44
37
  timestamp: Date.now(),
45
38
  latestVersions: updates,
46
- updatesAvailable: updates // Main thread will filter this properly later or we just mock it for architecture
39
+ updatesAvailable: updates, // Main thread will filter this properly later or we just mock it for architecture
47
40
  };
48
41
  fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
49
42
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testspectra/cli",
3
- "version": "1.1.8-rc.24",
4
- "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
3
+ "version": "1.1.8-rc.28",
4
+ "description": "TestSpectra Cross-Platform Test Runner CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -26,9 +26,9 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@testspectra/matchers": "^1.1.8-rc.24",
30
- "@testspectra/react": "^1.1.8-rc.24",
31
- "@testspectra/skills": "^1.1.8-rc.24",
29
+ "@testspectra/matchers": "^1.1.8-rc.28",
30
+ "@testspectra/react": "^1.1.8-rc.28",
31
+ "@testspectra/skills": "^1.1.8-rc.28",
32
32
  "chalk": "^5.3.0",
33
33
  "commander": "^12.1.0",
34
34
  "dotenv": "^16.4.5",
@@ -38,10 +38,9 @@
38
38
  "zod": "^3.23.8"
39
39
  },
40
40
  "optionalDependencies": {
41
- "@testspectra/cli-darwin-arm64": "1.1.8-rc.24",
42
- "@testspectra/cli-linux-x64": "1.1.8-rc.24",
43
- "@testspectra/cli-win32-arm64": "1.1.8-rc.24",
44
- "@testspectra/cli-win32-x64": "1.1.8-rc.24"
41
+ "@testspectra/cli-darwin-arm64": "1.1.8-rc.28",
42
+ "@testspectra/cli-linux-x64": "1.1.8-rc.28",
43
+ "@testspectra/cli-win32-x64": "1.1.8-rc.28"
45
44
  },
46
45
  "devDependencies": {
47
46
  "@types/node": "^20.14.0",