@testspectra/cli 1.1.8-rc.3 → 1.1.8-rc.30

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 (247) hide show
  1. package/README.md +50 -0
  2. package/bin/.testspectra-runner.hash +1 -1
  3. package/bin/spectra.js +0 -0
  4. package/dist/commands/__tests__/doctor-filter.test.js +37 -0
  5. package/dist/commands/__tests__/doctor-scope.test.d.ts +1 -0
  6. package/dist/commands/__tests__/doctor-scope.test.js +24 -0
  7. package/dist/commands/__tests__/run-e2e.test.js +49 -49
  8. package/dist/commands/__tests__/test-error-streaming.test.d.ts +1 -0
  9. package/dist/commands/__tests__/test-error-streaming.test.js +85 -0
  10. package/dist/commands/__tests__/test-scaffolding.test.d.ts +1 -0
  11. package/dist/commands/__tests__/test-scaffolding.test.js +32 -0
  12. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  13. package/dist/commands/__tests__/upgrade.test.js +95 -0
  14. package/dist/commands/add.js +5 -4
  15. package/dist/commands/devices.js +1 -0
  16. package/dist/commands/docs.d.ts +4 -0
  17. package/dist/commands/docs.js +17 -0
  18. package/dist/commands/doctor.d.ts +9 -1
  19. package/dist/commands/doctor.js +17 -3
  20. package/dist/commands/init.js +7 -160
  21. package/dist/commands/install.js +5 -0
  22. package/dist/commands/license.js +1 -0
  23. package/dist/commands/test.d.ts +13 -0
  24. package/dist/commands/test.js +115 -21
  25. package/dist/commands/upgrade.d.ts +12 -0
  26. package/dist/commands/upgrade.js +197 -0
  27. package/dist/generator/__tests__/tsconfig-isolation.test.js +2 -1
  28. package/dist/generator/__tests__/type-generator.test.js +38 -0
  29. package/dist/generator/architecture-doc-generator.d.ts +14 -0
  30. package/dist/generator/architecture-doc-generator.js +80 -0
  31. package/dist/generator/index.d.ts +1 -0
  32. package/dist/generator/index.js +1 -0
  33. package/dist/generator/tsconfig-generator.js +1 -1
  34. package/dist/generator/type-generator.d.ts +5 -2
  35. package/dist/generator/type-generator.js +14 -17
  36. package/dist/index.js +54 -26
  37. package/dist/linter/__tests__/component-test-imports.test.js +51 -51
  38. package/dist/linter/__tests__/shared-lib-boundary.test.js +15 -15
  39. package/dist/linter/__tests__/spec-schema.test.js +167 -167
  40. package/dist/linter/discovery.d.ts +3 -3
  41. package/dist/linter/discovery.js +7 -7
  42. package/dist/linter/schemas/spec.schema.d.ts +8 -8
  43. package/dist/linter/schemas/suite.schema.d.ts +4 -4
  44. package/dist/runner/bridge.d.ts +1 -1
  45. package/dist/runner/bridge.js +101 -11
  46. package/dist/runner/reporter.d.ts +1 -0
  47. package/dist/runner/reporter.js +12 -6
  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/update-notifier.d.ts +2 -0
  53. package/dist/utils/update-notifier.js +67 -0
  54. package/package.json +34 -30
  55. package/templates/default/CLAUDE.md +16 -16
  56. package/templates/default/global-hooks/after/mobile.hook.ts +3 -3
  57. package/templates/default/global-hooks/after/web.hook.ts +3 -3
  58. package/templates/default/global-hooks/before/mobile.hook.ts +3 -3
  59. package/templates/default/global-hooks/before/web.hook.ts +3 -3
  60. package/templates/default/package.json +2 -2
  61. package/templates/default/page-objects/AuthPage/mobile.ts +5 -5
  62. package/templates/default/page-objects/AuthPage/web.ts +10 -10
  63. package/templates/default/page-objects/MatchersPage/mobile.ts +70 -70
  64. package/templates/default/page-objects/MatchersPage/web.ts +43 -43
  65. package/templates/default/page-objects/NavigationPage/mobile.ts +14 -14
  66. package/templates/default/page-objects/NavigationPage/web.ts +8 -8
  67. package/templates/default/page-objects/PlaygroundPage/mobile.ts +42 -42
  68. package/templates/default/page-objects/PlaygroundPage/web.ts +31 -31
  69. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -53
  70. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -19
  71. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -31
  72. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -3
  73. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -3
  74. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -27
  75. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -19
  76. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -26
  77. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -30
  78. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -22
  79. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -27
  80. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -20
  81. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -24
  82. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -26
  83. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -37
  84. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -21
  85. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -29
  86. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -16
  87. package/templates/default/specs/ApiInterception/suite.md +10 -10
  88. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -30
  89. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -23
  90. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -16
  91. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -28
  92. package/templates/default/specs/ApiSeeding/suite.md +9 -9
  93. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -7
  94. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -19
  95. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -9
  96. package/templates/default/specs/Authentication/suite.md +9 -9
  97. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +9 -9
  98. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +17 -17
  99. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  100. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -17
  101. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -28
  102. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -26
  103. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -26
  104. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +19 -19
  105. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +11 -11
  106. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +17 -17
  107. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  108. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -14
  109. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -21
  110. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -28
  111. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -29
  112. package/templates/default/specs/DeepLink/suite.md +9 -9
  113. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +11 -11
  114. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +17 -17
  115. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  116. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +29 -29
  117. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +19 -19
  118. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  119. package/templates/default/specs/ElementMatchers/suite.md +10 -10
  120. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -5
  121. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -16
  122. package/templates/default/specs/EnvironmentConfig/suite.md +9 -9
  123. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -26
  124. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -22
  125. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -13
  126. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -20
  127. package/templates/default/specs/PermissionRationale/suite.md +9 -9
  128. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +6 -6
  129. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +18 -18
  130. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  131. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +6 -6
  132. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +18 -18
  133. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  134. package/templates/default/spectra.config.ts +52 -52
  135. package/templates/default/support/actions/fillCredentials/mobile.action.ts +4 -4
  136. package/templates/default/support/actions/fillCredentials/web.action.ts +4 -4
  137. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -9
  138. package/templates/default/support/actions/seedSession/web.action.ts +13 -13
  139. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +5 -5
  140. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  141. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +6 -6
  142. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  143. package/templates/docs/ARCHITECTURE.default.md +26 -0
  144. package/templates/docs/ARCHITECTURE.nx.md +119 -0
  145. package/templates/nx/.nx/workspace-data/{59842386-c7c4-44ca-b2c2-20e1700bd13d.db-shm → 70094CFD-E89B-57A1-9619-2FC5F4963C54.db} +0 -0
  146. package/templates/nx/.nx/workspace-data/d/daemon.log +1012 -1612
  147. package/templates/nx/.nx/workspace-data/file-map.json +123 -137
  148. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -1
  149. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  150. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +535 -535
  151. package/templates/nx/.nx/workspace-data/project-graph.json +592 -591
  152. package/templates/nx/.nx/workspace-data/source-maps.json +336 -0
  153. package/templates/nx/CLAUDE.md +10 -10
  154. package/templates/nx/package.json +3 -3
  155. package/templates/nx/packages/matchers/e2e/project.json +2 -2
  156. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -27
  157. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -27
  158. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -24
  159. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -24
  160. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +3 -3
  161. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +19 -19
  162. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +3 -3
  163. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +17 -17
  164. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +3 -3
  165. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +11 -11
  166. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +3 -3
  167. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +11 -11
  168. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +3 -3
  169. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +29 -29
  170. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -5
  171. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -16
  172. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -9
  173. package/templates/nx/packages/playground/e2e/project.json +2 -2
  174. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +3 -3
  175. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +6 -6
  176. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +3 -3
  177. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +6 -6
  178. package/templates/nx/pnpm-lock.yaml +6663 -6663
  179. package/templates/nx/pnpm-workspace.yaml +9 -9
  180. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +33 -33
  181. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +91 -91
  182. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +5 -5
  183. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +8 -8
  184. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +26 -26
  185. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +31 -31
  186. package/templates/nx/shared/testing/project.json +18 -19
  187. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +4 -4
  188. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +4 -4
  189. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +3 -3
  190. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +5 -5
  191. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +4 -4
  192. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +6 -6
  193. package/templates/nx/spectra.config.ts +52 -52
  194. package/templates/react-component/.cursorrules +17 -17
  195. package/templates/react-component/.vscode/extensions.json +5 -5
  196. package/templates/react-component/CLAUDE.md +14 -14
  197. package/templates/react-component/README.md +69 -69
  198. package/templates/react-component/fixtures/component-mock.json +12 -12
  199. package/templates/react-component/global-hooks/after/web.hook.ts +3 -3
  200. package/templates/react-component/global-hooks/before/web.hook.ts +3 -3
  201. package/templates/react-component/package.json +27 -27
  202. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -23
  203. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -23
  204. package/templates/react-component/page-objects/InputComponent/web.ts +13 -13
  205. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -18
  206. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -11
  207. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -18
  208. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -10
  209. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -18
  210. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -16
  211. package/templates/react-component/specs/BadgeComponent/suite.md +12 -12
  212. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -19
  213. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +25 -28
  214. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -19
  215. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +13 -22
  216. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -18
  217. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -8
  218. package/templates/react-component/specs/ButtonComponent/suite.md +12 -12
  219. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -20
  220. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +24 -28
  221. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -18
  222. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -9
  223. package/templates/react-component/specs/InputComponent/suite.md +12 -12
  224. package/templates/react-component/spectra.config.ts +17 -27
  225. package/templates/react-component/src/components/Badge/Badge.tsx +60 -60
  226. package/templates/react-component/src/components/Button/Button.tsx +59 -57
  227. package/templates/react-component/src/components/Input/Input.tsx +40 -41
  228. package/templates/react-component/src/test-utils.tsx +23 -23
  229. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -9
  230. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -12
  231. package/bin/testspectra-runner.exe +0 -0
  232. package/dist/.tsbuildinfo +0 -1
  233. package/dist/commands/update.d.ts +0 -3
  234. package/dist/commands/update.js +0 -109
  235. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +0 -290
  236. package/dist/lifecycle/hook-discovery.d.ts +0 -24
  237. package/dist/lifecycle/hook-discovery.js +0 -60
  238. package/dist/lifecycle/hook-dispatcher.d.ts +0 -34
  239. package/dist/lifecycle/hook-dispatcher.js +0 -190
  240. package/dist/lifecycle/index.d.ts +0 -3
  241. package/dist/lifecycle/index.js +0 -3
  242. package/dist/lifecycle/parallel-grouping.d.ts +0 -27
  243. package/dist/lifecycle/parallel-grouping.js +0 -151
  244. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db +0 -0
  245. package/templates/nx/.nx/workspace-data/59842386-c7c4-44ca-b2c2-20e1700bd13d.db-wal +0 -0
  246. package/templates/nx/.nx/workspace-data/d/server-process.json +0 -3
  247. /package/dist/{lifecycle/__tests__/lifecycle-engine.test.d.ts → commands/__tests__/doctor-filter.test.d.ts} +0 -0
@@ -6,6 +6,7 @@ import chalk from 'chalk';
6
6
  import { ConfigLoader } from '../config/loader.js';
7
7
  import { TypeGenerator } from '../types/generator.js';
8
8
  import { TsConfigGenerator } from '../generator/tsconfig-generator.js';
9
+ import { generateArchitectureDoc } from '../generator/architecture-doc-generator.js';
9
10
  function parsePnpmWorkspaceGlobs(content) {
10
11
  const lines = content.split('\n');
11
12
  const globs = [];
@@ -460,6 +461,8 @@ export async function initCommand(options = {}) {
460
461
  (typeof rootPkg.devDependencies['@testspectra/matchers'] === 'string' &&
461
462
  rootPkg.devDependencies['@testspectra/matchers'].startsWith('link:')))
462
463
  rootPkg.devDependencies['@testspectra/matchers'] = cliDepVersion;
464
+ if (!rootPkg.devDependencies['nx'])
465
+ rootPkg.devDependencies['nx'] = '^20.8.4';
463
466
  if (!rootPkg.devDependencies['typescript'])
464
467
  rootPkg.devDependencies['typescript'] = '^5.4.5';
465
468
  if (!rootPkg.devDependencies['@wdio/cli'])
@@ -508,6 +511,7 @@ export async function initCommand(options = {}) {
508
511
  copyRecursive(sampleE2eSrc, targetE2eDir, {
509
512
  'playground-e2e': e2eProjectName,
510
513
  'packages/playground/e2e': path.relative(cwd, targetE2eDir).replace(/\\/g, '/'),
514
+ 'testspectra:scope:playground': `testspectra:scope:${featureName}`,
511
515
  'scope:playground': `scope:${featureName}`,
512
516
  });
513
517
  // Adjust project.json
@@ -519,7 +523,7 @@ export async function initCommand(options = {}) {
519
523
  pObj.sourceRoot = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
520
524
  pObj.targets.e2e.options.cwd = path.relative(cwd, targetE2eDir).replace(/\\/g, '/');
521
525
  pObj.targets['type-check'].options.cwd = '.';
522
- pObj.tags = [`scope:${featureName}`, 'type:e2e'];
526
+ pObj.tags = ['testspectra:e2e', `testspectra:scope:${featureName}`];
523
527
  fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
524
528
  }
525
529
  catch { }
@@ -557,137 +561,7 @@ export async function initCommand(options = {}) {
557
561
  // 7. Generate Ambient Types in ROOT only
558
562
  TypeGenerator.writeDeclarationFiles(cwd);
559
563
  ensureVsCodeExtensions(cwd);
560
- const archDocContent = `# TestSpectra Enterprise Monorepo Architecture
561
-
562
- This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
563
-
564
- ---
565
-
566
- ## 1. Directory & File Overview
567
-
568
- ### Centralized Root Elements
569
- - \`./spectra.config.ts\`: Single source of truth for runtime configurations (Base URL, Appium devices, browser targets, timeouts).
570
- - \`./.testspectra/\`: Local test project cache, runtime metadata, auto-generated master TSConfig (\`.testspectra/tsconfig.json\`), platform sub-configs (\`.testspectra/tsconfig/\`), and universal ambient types (\`.testspectra/types/\`). Global binaries and drivers are managed at \`~/.testspectra/drivers/\`.
571
- - \`./nx.json\`: Nx target defaults with execution caching and inputs.
572
- - \`./pnpm-workspace.yaml\`: Monorepo package glob definitions.
573
- - \`./tsconfig.json\`: Solution-style TypeScript orchestrator referencing \`./.testspectra/tsconfig.json\`.
574
-
575
- ### Centralized Shared Testing Library (\`./${sharedTestingRelPath}\`)
576
- Provides cross-feature reusable test entities with **100% zero-import global resolution**:
577
- - \`page-objects/\`: Shared Page Object models (e.g. NavigationBar, AppHeader).
578
- - \`steps/\`: Cross-feature business step flows (e.g. \`Step.loginAsAdmin()\`).
579
- - \`actions/\`: Custom atomic actions (e.g. \`Spectra.dismissBanner()\`).
580
- - \`fixtures/\`: Common test data and environment configurations (e.g. \`Fixture.appConfig\`).
581
-
582
- ### Distributed Feature E2E Modules (${featureE2eDirs.length} Features)
583
- ${featureE2eDirs
584
- .map((d) => {
585
- const rel = path.relative(cwd, d);
586
- const name = path.basename(path.dirname(d));
587
- const e2eName = `${name}-${e2eFolderName}`;
588
- return `- \`./${rel}\` (**${name}**, Nx Project: \`${e2eName}\`):
589
- - \`specs/\`: Feature test cases (\`specs/<SuiteName>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
590
- - \`page-objects/\`: Feature-specific Page Objects (e.g. \`LoginPage\`, \`ProductPage\`).
591
- - \`project.json\`: Nx targets (\`e2e\`, \`type-check\`) supporting configurations (\`--configuration=android\`, \`--configuration=ios\`, \`--configuration=headless\`).`;
592
- })
593
- .join('\n')}
594
-
595
- ---
596
-
597
- ## 2. Zero-Import Consumption Flow & Mechanics
598
-
599
- TestSpectra completely eliminates boilerplate \`import\` statements across all test specs, steps, and page objects.
600
-
601
- ### How It Works:
602
- 1. **Centralized Ambient Aggregation**:
603
- TestSpectra Language Service Plugin & CLI automatically scan:
604
- - **Local Feature Entities**: \`<feature>/${e2eFolderName}/page-objects/\`, \`<feature>/${e2eFolderName}/steps/\`, \`<feature>/${e2eFolderName}/actions/\`
605
- - **Shared Library Entities**: \`./${sharedTestingRelPath}/page-objects/\`, \`./${sharedTestingRelPath}/steps/\`, \`./${sharedTestingRelPath}/actions/\`, \`./${sharedTestingRelPath}/fixtures/\`
606
- 2. **Ambient Typing Generation**:
607
- These are compiled into \`.testspectra/types/web.d.ts\`, \`android.d.ts\`, \`ios.d.ts\`, \`fixtures.d.ts\`.
608
- 3. **Instant IDE Autocomplete & Zero Clutter**:
609
- Specs consume both local feature objects and shared library utilities globally.
610
-
611
- ### Code Example:
612
- \`\`\`typescript
613
- // packages/features/auth/e2e/specs/Auth/TC-AUTH-01/web.test.ts
614
- // 🌟 NOTICE: 100% Zero-Import & Flat Execution! (No import statement, no describe wrapper)
615
-
616
- it("should navigate via shared header and login with local POM", async () => {
617
- // 1. Consumed from Shared Testing Library (${sharedTestingRelPath}/page-objects/NavigationBar)
618
- await NavigationBar.goToLogin();
619
-
620
- // 2. Consumed from Shared Testing Library (${sharedTestingRelPath}/steps/loginAsAdmin)
621
- await Step.loginAsAdmin();
622
-
623
- // 3. Consumed from Local Feature POM (page-objects/LoginPage)
624
- await LoginPage.flashAlert.shouldBeVisible();
625
- await LoginPage.flashAlert.shouldContainText("Welcome");
626
-
627
- // 4. Consumed from Shared Testing Library Custom Action (${sharedTestingRelPath}/actions/dismissBanner)
628
- await Spectra.dismissBanner();
629
- });
630
- \`\`\`
631
-
632
- ---
633
-
634
- ## 3. How to Run Tests
635
-
636
- You can execute tests either from the **Monorepo Root via Nx** or directly inside each **Feature Directory via CLI**:
637
-
638
- ### Option A: From Root Monorepo via Nx (Recommended for CI & Team Workflows)
639
-
640
- \`\`\`bash
641
- # 1. Run E2E for a SPECIFIC feature module (e.g. auth)
642
- pnpm nx run auth-${e2eFolderName}:e2e
643
-
644
- # 2. Run E2E for a specific feature on ANDROID or IOS
645
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=android
646
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=ios
647
-
648
- # 3. Run E2E for a specific feature in HEADLESS browser mode
649
- pnpm nx run auth-${e2eFolderName}:e2e --configuration=headless
650
-
651
- # 4. Run ALL feature E2E suites across the entire monorepo in parallel
652
- pnpm nx run-many -t e2e
653
-
654
- # 5. Run only E2E tests for features modified in current Git branch / PR
655
- pnpm nx affected -t e2e
656
- \`\`\`
657
-
658
- ### Option B: From Inside the Feature Directory (Direct CLI)
659
-
660
- \`\`\`bash
661
- # Navigate to the feature e2e folder
662
- cd ${featureE2eDirs.length > 0 ? path.relative(cwd, featureE2eDirs[0]).replace(/\\/g, '/') : `modules/auth/${e2eFolderName}`}
663
-
664
- # Run all specs in this feature
665
- pnpm spectra run
666
-
667
- # Run a specific test case file
668
- pnpm spectra run specs/Auth/TC-AUTH-01/web.test.ts
669
-
670
- # Target Android device or emulator
671
- pnpm spectra run --target android
672
-
673
- # Run in headed / interactive browser mode
674
- pnpm spectra run --no-headless
675
- \`\`\`
676
-
677
- ---
678
-
679
- ## 4. Type Checking & Validation
680
-
681
- \`\`\`bash
682
- # Type check all E2E projects via Nx
683
- pnpm nx run-many -t type-check
684
-
685
- # Or standard TypeScript project references build from root
686
- pnpm type-check
687
- # or: npx tsc -b
688
- \`\`\`
689
- `;
690
- fs.writeFileSync(path.join(cwd, 'ARCHITECTURE.md'), archDocContent, 'utf-8');
564
+ generateArchitectureDoc(cwd);
691
565
  // 8. Ensure root .gitignore ignores .testspectra/ and *.tsbuildinfo
692
566
  const rootGitignorePath = path.join(cwd, '.gitignore');
693
567
  const ignoreEntries = ['.testspectra/', '*.tsbuildinfo', '.nx/cache'];
@@ -741,34 +615,7 @@ pnpm type-check
741
615
  TypeGenerator.writeDeclarationFiles(cwd);
742
616
  ensureVsCodeExtensions(cwd);
743
617
  // Standalone Architecture Documentation
744
- const standaloneArchDoc = `# TestSpectra Project Architecture
745
-
746
- This project is built with the TestSpectra testing framework.
747
-
748
- ## Structure & File Map
749
- - \`./spectra.config.ts\`: Typed runner configuration.
750
- - \`./specs/\`: Layered test cases (\`specs/<Suite>/<CaseId>/\` with \`web.test.ts\`, \`android.test.ts\`, \`ios.test.ts\`).
751
- - \`./page-objects/\`: Page Object models per platform.
752
- - \`./steps/\`: Business flows accessible globally via \`Step.*\`.
753
- - \`./actions/\`: Atomic actions accessible globally via \`Spectra.*\`.
754
- - \`./fixtures/\`: JSON fixtures accessible globally via \`Fixture.*\`.
755
- - \`./global-hooks/\`: Global suite lifecycle hooks.
756
- - \`./.testspectra/\`: Auto-generated ambient typings and local runtime cache.
757
-
758
- ## Execution
759
- \`\`\`bash
760
- # Run web test suite
761
- pnpm test
762
-
763
- # Run Android / iOS
764
- pnpm spectra run --target android
765
- pnpm spectra run --target ios
766
-
767
- # Type check
768
- pnpm type-check
769
- \`\`\`
770
- `;
771
- fs.writeFileSync(path.join(cwd, 'ARCHITECTURE.md'), standaloneArchDoc, 'utf-8');
618
+ generateArchitectureDoc(cwd);
772
619
  // Safely update root README.md (do not overwrite existing content)
773
620
  const standaloneReadmePath = path.join(cwd, 'README.md');
774
621
  const standaloneArchSection = `\n## E2E Testing Architecture (TestSpectra)\n\nDetailed test architecture and folder map are documented in: \n👉 **[ARCHITECTURE.md](./ARCHITECTURE.md)**\n\n- **Run tests**: \`pnpm test\`\n- **Type-check**: \`pnpm type-check\`\n`;
@@ -17,6 +17,9 @@ export async function installCommand(item, options = {}) {
17
17
  if (item) {
18
18
  args.push(item);
19
19
  }
20
+ if (options.force) {
21
+ args.push('--force');
22
+ }
20
23
  return new Promise((resolve) => {
21
24
  let child;
22
25
  if (binPath === 'cargo') {
@@ -34,6 +37,7 @@ export async function installCommand(item, options = {}) {
34
37
  cwd: workspaceRoot,
35
38
  env: runnerEnv,
36
39
  stdio: 'inherit',
40
+ windowsHide: true,
37
41
  });
38
42
  }
39
43
  else {
@@ -41,6 +45,7 @@ export async function installCommand(item, options = {}) {
41
45
  cwd,
42
46
  env: runnerEnv,
43
47
  stdio: 'inherit',
48
+ windowsHide: true,
44
49
  });
45
50
  }
46
51
  child.on('close', (code) => {
@@ -14,6 +14,7 @@ export async function licenseCommand(action = 'status', key, options = {}) {
14
14
  const child = spawn(binPath, args, {
15
15
  stdio: 'inherit',
16
16
  env: process.env,
17
+ windowsHide: true,
17
18
  });
18
19
  child.on('close', (code) => {
19
20
  if (code === 0) {
@@ -1,5 +1,6 @@
1
1
  export interface TestCommandOptions {
2
2
  cwd?: string;
3
+ runner?: 'spectra' | 'vitest';
3
4
  headed?: boolean;
4
5
  headless?: boolean;
5
6
  stepDelay?: string | number;
@@ -8,6 +9,18 @@ export interface TestCommandOptions {
8
9
  output?: string;
9
10
  concurrency?: string | number;
10
11
  }
12
+ /**
13
+ * Guarantees the `.testspectra` solution-tsconfig scaffolding exists before Vitest starts.
14
+ *
15
+ * The bundled Vitest browser runner transpiles raw TypeScript specs/page-objects Just-In-Time.
16
+ * When it does, `vite:esbuild` walks the nearest `tsconfig.json` and tries to read every entry in
17
+ * its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
18
+ * a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
19
+ * throws `ENOENT`, Vite answers HTTP 500, and every suite dies with "Failed to fetch dynamically
20
+ * imported module" before a single assertion runs. Regenerating the scaffolding up front (same
21
+ * output as `spectra sync-types`) keeps the reference resolvable.
22
+ */
23
+ export declare function ensureTestSpectraScaffolding(cwd: string): void;
11
24
  /**
12
25
  * `spectra test` — delegates to `@testspectra/react`'s bundled Vitest browser-mode runner for
13
26
  * component testing. Deliberately separate from `spectra run` (E2E via core/orchestrator): see
@@ -1,9 +1,30 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import chalk from 'chalk';
3
4
  import * as p from '@clack/prompts';
5
+ import { ConfigLoader } from '../config/loader.js';
4
6
  import { renderSummaryView } from '../reporter/summary-view.js';
5
7
  import { Reporter } from '../runner/reporter.js';
8
+ import { RustCoreBridge } from '../runner/bridge.js';
9
+ import { TypeGenerator } from '../generator/type-generator.js';
6
10
  import { runSystemChecks } from './doctor.js';
11
+ /**
12
+ * Guarantees the `.testspectra` solution-tsconfig scaffolding exists before Vitest starts.
13
+ *
14
+ * The bundled Vitest browser runner transpiles raw TypeScript specs/page-objects Just-In-Time.
15
+ * When it does, `vite:esbuild` walks the nearest `tsconfig.json` and tries to read every entry in
16
+ * its `references` — including `./.testspectra/tsconfig.json`. That directory is gitignored, so on
17
+ * a clean clone / fresh CI checkout the reference points at a file that does not exist, esbuild
18
+ * throws `ENOENT`, Vite answers HTTP 500, and every suite dies with "Failed to fetch dynamically
19
+ * imported module" before a single assertion runs. Regenerating the scaffolding up front (same
20
+ * output as `spectra sync-types`) keeps the reference resolvable.
21
+ */
22
+ export function ensureTestSpectraScaffolding(cwd) {
23
+ const masterConfigPath = path.join(cwd, '.testspectra', 'tsconfig.json');
24
+ if (fs.existsSync(masterConfigPath))
25
+ return;
26
+ TypeGenerator.generateAll(cwd);
27
+ }
7
28
  /**
8
29
  * Finds the Chrome-for-Testing binary that `spectra doctor` already installs/manages for the
9
30
  * E2E CDP driver (`core/driver-cdp`) — same install, same binary. `@testspectra/react` must not
@@ -14,8 +35,10 @@ import { runSystemChecks } from './doctor.js';
14
35
  */
15
36
  async function resolveBrowserExecutable(cwd) {
16
37
  try {
17
- const { dependencies } = await runSystemChecks(cwd);
18
- const chrome = dependencies.find((d) => d.category === 'web' && (d.name.includes('Chrome') || d.name.includes('Chromium')));
38
+ // `--only chrome` runs exactly one check — no adb/bun/git spawns, no Matchers lookup — since
39
+ // this call exists solely to locate the managed browser binary.
40
+ const { dependencies } = await runSystemChecks(cwd, { only: 'chrome' });
41
+ const chrome = dependencies.find((d) => d.category === 'web' && d.name.includes('Chrome'));
19
42
  return chrome?.installed && chrome.location ? chrome.location : null;
20
43
  }
21
44
  catch {
@@ -30,6 +53,11 @@ async function resolveBrowserExecutable(cwd) {
30
53
  */
31
54
  export async function testCommand(options = {}) {
32
55
  const cwd = options.cwd ?? process.cwd();
56
+ // User-facing phase log. Deliberately describes TestSpectra's own stages only — never the
57
+ // bundled runtime/toolchain underneath — so the CLI surface stays implementation-agnostic.
58
+ const logPhase = (message) => p.log.step(chalk.dim(message));
59
+ logPhase('Preparing component test workspace…');
60
+ ensureTestSpectraScaffolding(cwd);
33
61
  let reactPkg;
34
62
  try {
35
63
  reactPkg = await import('@testspectra/react');
@@ -41,6 +69,7 @@ export async function testCommand(options = {}) {
41
69
  process.exit(1);
42
70
  return;
43
71
  }
72
+ logPhase('Resolving managed test browser…');
44
73
  const browserExecutablePath = await resolveBrowserExecutable(cwd);
45
74
  if (!browserExecutablePath) {
46
75
  p.log.error(chalk.red('✖ TestSpectra-managed Chrome for Testing was not found.'));
@@ -64,6 +93,21 @@ export async function testCommand(options = {}) {
64
93
  else if (options.headless !== undefined) {
65
94
  headless = options.headless;
66
95
  }
96
+ // Fallback to spectra.config.ts if options were not explicitly provided on CLI
97
+ try {
98
+ const config = await ConfigLoader.loadConfig(cwd);
99
+ if (stepDelayMs === undefined && config?.webConfig?.stepDelay !== undefined) {
100
+ const parsed = parseInt(String(config.webConfig.stepDelay), 10);
101
+ if (!isNaN(parsed) && parsed >= 0)
102
+ stepDelayMs = parsed;
103
+ }
104
+ if (!options.headed && options.headless === undefined && config?.webConfig?.headless !== undefined) {
105
+ headless = String(config.webConfig.headless) !== 'false' && Boolean(config.webConfig.headless);
106
+ }
107
+ }
108
+ catch {
109
+ /* ignore config load failures */
110
+ }
67
111
  const concurrency = options.concurrency !== undefined ? parseInt(String(options.concurrency), 10) : undefined;
68
112
  const outputFile = options.output ? path.resolve(cwd, options.output) : undefined;
69
113
  // Reuses the exact same semantic terminal reporter `spectra run` drives (see
@@ -72,31 +116,81 @@ export async function testCommand(options = {}) {
72
116
  // / setup.ts), so the same Reporter class parses them with zero changes on either side.
73
117
  const reporter = new Reporter();
74
118
  const runStartedAt = Date.now();
75
- const ok = await reactPkg.runComponentTests({
76
- cwd,
77
- headless,
78
- stepDelayMs,
79
- spec: options.spec,
80
- outputFile,
81
- concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
82
- browserExecutablePath,
83
- onLine: (line) => {
84
- if (!line.startsWith('[TESTSPECTRA_'))
85
- return;
86
- reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'INFO', message: line });
87
- if (!reporter.isInteractiveMode()) {
88
- console.log(line);
89
- }
90
- },
91
- });
119
+ let ok = false;
120
+ if (options.runner === 'vitest') {
121
+ logPhase('Starting component test runner (Vitest Browser Mode)…');
122
+ ok = await reactPkg.runComponentTests({
123
+ runner: 'vitest',
124
+ cwd,
125
+ headless,
126
+ stepDelayMs,
127
+ spec: options.spec,
128
+ outputFile,
129
+ concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
130
+ browserExecutablePath,
131
+ onLine: (line) => {
132
+ const cleanLine = line.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '').trim();
133
+ if (!cleanLine)
134
+ return;
135
+ if (cleanLine.startsWith('[TESTSPECTRA_')) {
136
+ reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'INFO', message: cleanLine });
137
+ if (!reporter.isInteractiveMode()) {
138
+ console.log(cleanLine);
139
+ }
140
+ return;
141
+ }
142
+ if (reactPkg.isVitestNoise?.(cleanLine)) {
143
+ return;
144
+ }
145
+ const sanitized = reactPkg.sanitizeErrorMessage?.(cleanLine) ?? cleanLine;
146
+ reporter.addLog({ timestamp: new Date().toLocaleTimeString(), level: 'ERROR', message: sanitized });
147
+ if (!reporter.isInteractiveMode()) {
148
+ console.error(chalk.red(sanitized));
149
+ }
150
+ },
151
+ });
152
+ }
153
+ else {
154
+ logPhase('Starting component test runner (Spectra Native CDP Engine)…');
155
+ const bundleServer = await reactPkg.startBundleServer({
156
+ testRoot: cwd,
157
+ stepDelayMs: stepDelayMs ?? 0,
158
+ });
159
+ try {
160
+ const home = process.env.HOME || process.env.USERPROFILE || '';
161
+ const globalAppDataPath = path.join(home, '.testspectra');
162
+ const appDataPath = globalAppDataPath;
163
+ const runResult = await RustCoreBridge.run({
164
+ baseDir: cwd,
165
+ appDataPath,
166
+ platform: 'component',
167
+ config: {
168
+ bundleServerUrl: bundleServer.url,
169
+ webConfig: {
170
+ headless,
171
+ stepDelay: stepDelayMs,
172
+ browserExecutablePath,
173
+ },
174
+ },
175
+ specs: options.spec ? [options.spec] : undefined,
176
+ concurrency: concurrency !== undefined && !isNaN(concurrency) ? concurrency : undefined,
177
+ outputJsonPath: outputFile,
178
+ }, reporter);
179
+ ok = runResult.status.toLowerCase() === 'passed';
180
+ }
181
+ finally {
182
+ bundleServer.stop();
183
+ }
184
+ }
92
185
  const durationMs = Date.now() - runStartedAt;
93
186
  const result = reporter.generateResult(ok ? 'passed' : 'failed', `${(durationMs / 1000).toFixed(1)}s`);
94
- const totalTests = result.passedCount + result.failedCount;
187
+ const failedCount = !ok && result.failedCount === 0 ? 1 : result.failedCount;
188
+ const totalTests = result.passedCount + failedCount;
95
189
  renderSummaryView({
96
190
  totalSuites: totalTests,
97
191
  totalTests,
98
192
  passedCount: result.passedCount,
99
- failedCount: result.failedCount,
193
+ failedCount,
100
194
  durationMs,
101
195
  reportLocation: outputFile ? path.relative(cwd, outputFile) : '(use -o/--output to save a report)',
102
196
  });
@@ -0,0 +1,12 @@
1
+ export interface PackageUpdate {
2
+ pkg: string;
3
+ current: string | null;
4
+ latest: string | null;
5
+ needsUpdate: boolean;
6
+ }
7
+ export declare function upgradeCommand(requestedPackages?: string[], options?: {
8
+ check?: boolean;
9
+ packagesOnly?: boolean;
10
+ driversOnly?: boolean;
11
+ json?: boolean;
12
+ }): Promise<void>;
@@ -0,0 +1,197 @@
1
+ import { execSync, execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { RustCoreBridge } from '../runner/bridge.js';
6
+ import { checkDriverUpdates } from '../utils/dependency-updates.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
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
+ }
21
+ function getLatestVersion(pkg) {
22
+ try {
23
+ const out = execFileSync('npm', ['view', pkg, 'version', '--json'], {
24
+ stdio: ['ignore', 'pipe', 'ignore'],
25
+ timeout: 10000,
26
+ })
27
+ .toString()
28
+ .trim()
29
+ .replace(/"/g, '');
30
+ return out || null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ function getCurrentVersion(pkg, cwd) {
37
+ let cur = path.resolve(cwd);
38
+ while (cur && cur !== path.dirname(cur)) {
39
+ const pkgJson = path.join(cur, 'node_modules', pkg, 'package.json');
40
+ if (fs.existsSync(pkgJson)) {
41
+ try {
42
+ const p = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
43
+ return p.version ?? null;
44
+ }
45
+ catch { }
46
+ }
47
+ cur = path.dirname(cur);
48
+ }
49
+ return null;
50
+ }
51
+ function detectPackageManager(cwd) {
52
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
53
+ return 'pnpm';
54
+ if (fs.existsSync(path.join(cwd, 'bun.lockb')) || fs.existsSync(path.join(cwd, 'bun.lock')))
55
+ return 'bun';
56
+ return 'npm';
57
+ }
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) {
73
+ const list = pkgSpecs.join(' ');
74
+ switch (pm) {
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
+ }
79
+ case 'bun':
80
+ return `bun add -d ${list}`;
81
+ case 'npm':
82
+ default:
83
+ return `npm install -D ${list}`;
84
+ }
85
+ }
86
+ export async function upgradeCommand(requestedPackages = [], options = {}) {
87
+ const cwd = process.cwd();
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
+ }
104
+ console.log('\x1b[36m┌ 🚀 TestSpectra Self-Upgrade Mechanism\x1b[0m');
105
+ console.log('\x1b[36m│\x1b[0m');
106
+ const updates = [];
107
+ let packagesUpdated = false;
108
+ // Phase 1: NPM Packages
109
+ if (!options.driversOnly) {
110
+ console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 1: Workspace Packages\x1b[0m (Manager: \x1b[33m${pm}\x1b[0m)`);
111
+ console.log('\x1b[36m│\x1b[0m Fetching latest versions from npm registry...');
112
+ updates.push(...collectPackageUpdates(cwd, selected));
113
+ for (const { pkg, current, latest, needsUpdate } of updates) {
114
+ const currentStr = current ? `\x1b[33mv${current}\x1b[0m` : '\x1b[90m(not installed)\x1b[0m';
115
+ const latestStr = latest ? `\x1b[32mv${latest}\x1b[0m` : '\x1b[31m(unavailable)\x1b[0m';
116
+ const arrow = needsUpdate ? ` → ${latestStr}` : '';
117
+ const status = !latest ? '\x1b[31m✗\x1b[0m' : needsUpdate ? '\x1b[33m↑\x1b[0m' : '\x1b[32m✓\x1b[0m';
118
+ console.log(`\x1b[36m│\x1b[0m ${status} ${pkg.padEnd(28)} ${currentStr}${arrow}`);
119
+ }
120
+ const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
121
+ console.log('\x1b[36m│\x1b[0m');
122
+ if (options.check) {
123
+ console.log(`\x1b[36m│\x1b[0m \x1b[33m${toUpdate.length} package update(s) available. Run \x1b[36mspectra upgrade\x1b[33m to install.\x1b[0m`);
124
+ }
125
+ else if (toUpdate.length > 0) {
126
+ const pkgSpecs = toUpdate.map((u) => `${u.pkg}@${u.latest}`);
127
+ const installCmd = buildInstallCmd(pm, pkgSpecs, cwd);
128
+ console.log(`\x1b[36m│\x1b[0m Running: \x1b[90m${installCmd}\x1b[0m`);
129
+ try {
130
+ execSync(installCmd, { cwd, stdio: 'inherit' });
131
+ packagesUpdated = true;
132
+ }
133
+ catch (err) {
134
+ console.error(`\x1b[31m│ ✗ Package upgrade failed: ${err.message}\x1b[0m`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ else {
139
+ console.log('\x1b[36m│\x1b[0m \x1b[32m✓ All TestSpectra packages are already up-to-date!\x1b[0m');
140
+ }
141
+ console.log('\x1b[36m│\x1b[0m');
142
+ }
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
+ }
154
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m\n');
155
+ return;
156
+ }
157
+ // Phase 2: Drivers and Binaries
158
+ if (!options.packagesOnly && !targeted) {
159
+ console.log(`\x1b[36m│\x1b[0m \x1b[1mPhase 2: System Binaries & Drivers\x1b[0m`);
160
+ try {
161
+ const binPath = RustCoreBridge.resolveBinaryPath();
162
+ // Using doctor --fix as the underlying mechanism for pulling drivers
163
+ // In the future, this could be passed a specific --force-latest flag
164
+ console.log(`\x1b[36m│\x1b[0m Forcing update for ADB and Spectra Android Agent...`);
165
+ const home = process.env.HOME || process.env.USERPROFILE || '';
166
+ const globalDriversDir = path.join(home, '.testspectra', 'drivers');
167
+ execSync(`"${binPath}" doctor --fix`, {
168
+ cwd,
169
+ stdio: 'inherit',
170
+ env: {
171
+ ...process.env,
172
+ TEST_SPECTRA_DRIVER_CACHE: globalDriversDir,
173
+ TEST_SPECTRA_FORCE_LATEST: '1',
174
+ },
175
+ });
176
+ console.log('\x1b[36m│\x1b[0m \x1b[32m✓ Drivers updated successfully.\x1b[0m');
177
+ }
178
+ catch (err) {
179
+ console.error(`\x1b[31m│ ✗ Driver upgrade failed: ${err.message}\x1b[0m`);
180
+ // We don't exit 1 here so we can still print the post-upgrade report
181
+ }
182
+ console.log('\x1b[36m│\x1b[0m');
183
+ }
184
+ // Phase 3: Post-Upgrade Report
185
+ console.log('\x1b[36m└──────────────────────────────────────────────────────────\x1b[0m');
186
+ console.log('\x1b[32m🎉 TestSpectra upgrade complete!\x1b[0m');
187
+ if (!options.driversOnly && packagesUpdated) {
188
+ const toUpdate = updates.filter((u) => u.needsUpdate && u.latest);
189
+ for (const u of toUpdate) {
190
+ console.log(` \x1b[32m✓\x1b[0m ${u.pkg}: v${u.current ?? '?'} → v${u.latest}`);
191
+ }
192
+ }
193
+ if (!options.packagesOnly) {
194
+ console.log(` \x1b[32m✓\x1b[0m System drivers and agents synchronized to latest versions`);
195
+ }
196
+ console.log('');
197
+ }