@testspectra/cli 1.0.69 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +36 -270
  3. package/bin/.testspectra-runner.hash +1 -0
  4. package/bin/spectra.js +1 -1
  5. package/bin/testspectra-runner +0 -0
  6. package/demo-app/assets/index-DPfv5_EF.js +116 -0
  7. package/demo-app/assets/index-gIwu2EMV.css +1 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
  11. package/dist/commands/__tests__/cloud-run.test.js +93 -0
  12. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  13. package/dist/commands/__tests__/doctor.test.js +39 -0
  14. package/dist/commands/__tests__/init.test.d.ts +1 -0
  15. package/dist/commands/__tests__/init.test.js +148 -0
  16. package/dist/commands/__tests__/license.test.d.ts +1 -0
  17. package/dist/commands/__tests__/license.test.js +13 -0
  18. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  19. package/dist/commands/__tests__/refactor.test.js +64 -0
  20. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run-e2e.test.js +181 -0
  22. package/dist/commands/__tests__/run.test.d.ts +1 -0
  23. package/dist/commands/__tests__/run.test.js +197 -0
  24. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  25. package/dist/commands/__tests__/watch.test.js +155 -0
  26. package/dist/commands/add.js +86 -85
  27. package/dist/commands/cloud-run.d.ts +11 -0
  28. package/dist/commands/cloud-run.js +118 -0
  29. package/dist/commands/demo.js +7 -7
  30. package/dist/commands/devices.d.ts +14 -2
  31. package/dist/commands/devices.js +44 -34
  32. package/dist/commands/doctor.d.ts +3 -1
  33. package/dist/commands/doctor.js +43 -264
  34. package/dist/commands/init.d.ts +1 -1
  35. package/dist/commands/init.js +206 -196
  36. package/dist/commands/install.d.ts +4 -0
  37. package/dist/commands/install.js +59 -0
  38. package/dist/commands/license.d.ts +4 -0
  39. package/dist/commands/license.js +30 -0
  40. package/dist/commands/refactor.d.ts +8 -0
  41. package/dist/commands/refactor.js +78 -0
  42. package/dist/commands/run.d.ts +22 -1
  43. package/dist/commands/run.js +252 -109
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +14 -0
  46. package/dist/commands/update.d.ts +3 -0
  47. package/dist/commands/update.js +109 -0
  48. package/dist/commands/watch.d.ts +6 -0
  49. package/dist/commands/watch.js +47 -43
  50. package/dist/config/loader.d.ts +1 -1
  51. package/dist/config/loader.js +40 -25
  52. package/dist/config/schema.d.ts +117 -49
  53. package/dist/config/schema.js +41 -34
  54. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  55. package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
  56. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  57. package/dist/generator/__tests__/type-generator.test.js +386 -0
  58. package/dist/generator/index.d.ts +2 -0
  59. package/dist/generator/index.js +2 -0
  60. package/dist/generator/tsconfig-generator.d.ts +27 -0
  61. package/dist/generator/tsconfig-generator.js +391 -0
  62. package/dist/generator/type-generator.d.ts +36 -0
  63. package/dist/generator/type-generator.js +644 -0
  64. package/dist/index.d.ts +9 -5
  65. package/dist/index.js +126 -47
  66. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  67. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  68. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  69. package/dist/lifecycle/hook-discovery.js +60 -0
  70. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  71. package/dist/lifecycle/hook-dispatcher.js +190 -0
  72. package/dist/lifecycle/index.d.ts +3 -0
  73. package/dist/lifecycle/index.js +3 -0
  74. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  75. package/dist/lifecycle/parallel-grouping.js +151 -0
  76. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  77. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  78. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  79. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  80. package/dist/mobile/device-pool.d.ts +23 -0
  81. package/dist/mobile/device-pool.js +1 -0
  82. package/dist/mobile/index.d.ts +1 -0
  83. package/dist/mobile/index.js +1 -0
  84. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  85. package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
  86. package/dist/refactor/index.d.ts +1 -0
  87. package/dist/refactor/index.js +1 -0
  88. package/dist/refactor/refactor-engine.d.ts +25 -0
  89. package/dist/refactor/refactor-engine.js +255 -0
  90. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  91. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  92. package/dist/reporter/semantic-formatter.d.ts +6 -0
  93. package/dist/reporter/semantic-formatter.js +191 -0
  94. package/dist/reporter/summary-view.d.ts +12 -0
  95. package/dist/reporter/summary-view.js +19 -0
  96. package/dist/reporter/terminal-stream.d.ts +32 -0
  97. package/dist/reporter/terminal-stream.js +82 -0
  98. package/dist/reporter/types.d.ts +2 -0
  99. package/dist/reporter/types.js +1 -0
  100. package/dist/runner/bridge.d.ts +10 -5
  101. package/dist/runner/bridge.js +69 -44
  102. package/dist/runner/reporter.d.ts +30 -2
  103. package/dist/runner/reporter.js +419 -13
  104. package/dist/server/demo.js +43 -44
  105. package/dist/types/generator.d.ts +1 -17
  106. package/dist/types/generator.js +1 -443
  107. package/dist/utils/demo-server.js +38 -35
  108. package/package.json +20 -12
  109. package/templates/default/CLAUDE.md +7 -5
  110. package/templates/default/fixtures/apiData.json +24 -0
  111. package/templates/default/package.json +13 -4
  112. package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
  113. package/templates/default/page-objects/MatchersPage/web.ts +14 -0
  114. package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
  115. package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
  116. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  117. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
  118. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  119. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  120. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
  121. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  122. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  123. package/templates/default/specs/ApiInterception/suite.md +9 -0
  124. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  125. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
  127. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  129. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  130. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  131. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  132. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  133. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  134. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  135. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  136. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  137. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  138. package/templates/default/spectra.config.ts +21 -14
  139. package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
  140. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
  141. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  142. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
  143. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  144. package/templates/default/tsconfig.json +3 -1
  145. package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
  146. package/templates/nx/CLAUDE.md +2 -2
  147. package/templates/nx/package.json +7 -5
  148. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
  149. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  150. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  151. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
  152. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  153. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  154. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  155. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  156. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  157. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  158. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  159. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  160. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  161. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  162. package/templates/nx/pnpm-lock.yaml +6663 -0
  163. package/templates/nx/pnpm-workspace.yaml +2 -1
  164. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  165. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
  166. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
  167. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  168. package/templates/nx/shared/testing/project.json +19 -0
  169. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  170. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  171. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  172. package/templates/nx/spectra.config.ts +19 -12
  173. package/templates/nx/tsconfig.json +3 -1
  174. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  175. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  176. package/dist/index.cjs +0 -101
  177. package/dist/plugin.cjs +0 -575
  178. package/dist/plugin.d.ts +0 -16
  179. package/dist/plugin.js +0 -184
  180. package/dist/step/index.d.ts +0 -6
  181. package/dist/step/index.js +0 -6
  182. package/dist/step/matchers.d.ts +0 -14
  183. package/dist/step/matchers.js +0 -114
  184. package/dist/step/proto.d.ts +0 -46
  185. package/dist/step/proto.js +0 -114
  186. package/dist/step/runner/collection.d.ts +0 -13
  187. package/dist/step/runner/collection.js +0 -51
  188. package/dist/step/runner/single.d.ts +0 -21
  189. package/dist/step/runner/single.js +0 -138
  190. package/dist/step/spectra.d.ts +0 -34
  191. package/dist/step/spectra.js +0 -156
  192. package/dist/step/types.d.ts +0 -32
  193. package/dist/step/types.js +0 -5
  194. package/templates/default/tsconfig.spectra.android.json +0 -46
  195. package/templates/default/tsconfig.spectra.ios.json +0 -46
  196. package/templates/default/tsconfig.spectra.json +0 -22
  197. package/templates/default/tsconfig.spectra.web.json +0 -53
  198. package/templates/nx/fixtures/playgroundData.json +0 -7
  199. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  200. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  201. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  202. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  203. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  204. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  205. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  206. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  207. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  208. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  209. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  210. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  211. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  212. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  213. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  214. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  215. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  216. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  217. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  218. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  219. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  220. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  221. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  222. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  223. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  224. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  225. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  226. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  227. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  228. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  229. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  230. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  231. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  232. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  233. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  234. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  235. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  236. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  237. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  238. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  239. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  240. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  241. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  242. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  243. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  244. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  245. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  246. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  247. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  248. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  249. package/templates/nx/specs/Playground/suite.md +0 -9
  250. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  251. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  252. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  253. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  254. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  255. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  256. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  257. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  258. package/templates/nx/tsconfig.spectra.android.json +0 -39
  259. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  260. package/templates/nx/tsconfig.spectra.json +0 -25
  261. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  262. package/templates/nx/tsconfig.spectra.web.json +0 -45
  263. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  264. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  265. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  266. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  267. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  268. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  269. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  270. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  271. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  272. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  273. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  274. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  275. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  276. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  277. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  278. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -1,26 +1,26 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- import * as p from "@clack/prompts";
5
- import chalk from "chalk";
6
- import { TypeGenerator } from "../types/generator.js";
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import * as p from '@clack/prompts';
5
+ import chalk from 'chalk';
6
+ import { TypeGenerator } from '../types/generator.js';
7
7
  function parsePnpmWorkspaceGlobs(content) {
8
- const lines = content.split("\n");
8
+ const lines = content.split('\n');
9
9
  const globs = [];
10
10
  let inPackages = false;
11
11
  for (const line of lines) {
12
12
  const trimmed = line.trim();
13
- if (trimmed.startsWith("packages:")) {
13
+ if (trimmed.startsWith('packages:')) {
14
14
  inPackages = true;
15
15
  continue;
16
16
  }
17
17
  if (inPackages) {
18
- if (trimmed.startsWith("-")) {
19
- const item = trimmed.replace(/^-\s*['"]?/, "").replace(/['"]?\s*$/, "");
18
+ if (trimmed.startsWith('-')) {
19
+ const item = trimmed.replace(/^-\s*['"]?/, '').replace(/['"]?\s*$/, '');
20
20
  if (item)
21
21
  globs.push(item);
22
22
  }
23
- else if (trimmed && !trimmed.startsWith("#")) {
23
+ else if (trimmed && !trimmed.startsWith('#')) {
24
24
  break;
25
25
  }
26
26
  }
@@ -34,11 +34,11 @@ function scanDirectoriesForProjects(cwd, patterns) {
34
34
  if (currentDepth > maxDepth || !fs.existsSync(dir))
35
35
  return;
36
36
  const base = path.basename(dir);
37
- if (base === "node_modules" || base === "dist" || base === ".testspectra" || base === ".git" || base === "shared") {
37
+ if (base === 'node_modules' || base === 'dist' || base === '.testspectra' || base === '.git' || base === 'shared') {
38
38
  return;
39
39
  }
40
- const hasPkg = fs.existsSync(path.join(dir, "package.json"));
41
- const hasNx = fs.existsSync(path.join(dir, "project.json"));
40
+ const hasPkg = fs.existsSync(path.join(dir, 'package.json'));
41
+ const hasNx = fs.existsSync(path.join(dir, 'project.json'));
42
42
  if ((hasPkg || hasNx) && dir !== cwd) {
43
43
  const rel = path.relative(cwd, dir);
44
44
  if (!visited.has(rel)) {
@@ -46,7 +46,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
46
46
  let name = path.basename(dir);
47
47
  if (hasPkg) {
48
48
  try {
49
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
50
50
  if (pkg.name)
51
51
  name = pkg.name;
52
52
  }
@@ -54,7 +54,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
54
54
  }
55
55
  else if (hasNx) {
56
56
  try {
57
- const nxJson = JSON.parse(fs.readFileSync(path.join(dir, "project.json"), "utf-8"));
57
+ const nxJson = JSON.parse(fs.readFileSync(path.join(dir, 'project.json'), 'utf-8'));
58
58
  if (nxJson.name)
59
59
  name = nxJson.name;
60
60
  }
@@ -81,7 +81,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
81
81
  }
82
82
  if (patterns.length > 0) {
83
83
  for (const pat of patterns) {
84
- const cleanPat = pat.replace(/\/\*\*?$/, "").replace(/\/\*$/, "");
84
+ const cleanPat = pat.replace(/\/\*\*?$/, '').replace(/\/\*$/, '');
85
85
  const searchRoot = path.join(cwd, cleanPat);
86
86
  if (fs.existsSync(searchRoot)) {
87
87
  const stat = fs.statSync(searchRoot);
@@ -92,7 +92,7 @@ function scanDirectoriesForProjects(cwd, patterns) {
92
92
  }
93
93
  }
94
94
  else {
95
- for (const fallback of ["packages", "modules", "apps", "libs", "src"]) {
95
+ for (const fallback of ['packages', 'modules', 'apps', 'libs', 'src']) {
96
96
  const fbPath = path.join(cwd, fallback);
97
97
  if (fs.existsSync(fbPath)) {
98
98
  search(fbPath, 1, 3);
@@ -107,43 +107,43 @@ export async function addCommand(moduleNameArg, options = {}) {
107
107
  let rootDir = cwd;
108
108
  let cur = cwd;
109
109
  while (cur !== path.dirname(cur)) {
110
- if (fs.existsSync(path.join(cur, "spectra.config.ts")) ||
111
- fs.existsSync(path.join(cur, "pnpm-workspace.yaml")) ||
112
- fs.existsSync(path.join(cur, "nx.json"))) {
110
+ if (fs.existsSync(path.join(cur, 'spectra.config.ts')) ||
111
+ fs.existsSync(path.join(cur, 'pnpm-workspace.yaml')) ||
112
+ fs.existsSync(path.join(cur, 'nx.json'))) {
113
113
  rootDir = cur;
114
114
  break;
115
115
  }
116
116
  cur = path.dirname(cur);
117
117
  }
118
- p.intro(chalk.bold.cyan("✨ TestSpectra Module Generator"));
118
+ p.intro(chalk.bold.cyan('✨ TestSpectra Module Generator'));
119
119
  // Check if we are in a monorepo
120
- const pnpmWorkspacePath = path.join(rootDir, "pnpm-workspace.yaml");
121
- const nxJsonPath = path.join(rootDir, "nx.json");
120
+ const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml');
121
+ const nxJsonPath = path.join(rootDir, 'nx.json');
122
122
  const isMonorepo = fs.existsSync(pnpmWorkspacePath) || fs.existsSync(nxJsonPath);
123
123
  // 2. Resolve template directory
124
124
  const __filename = fileURLToPath(import.meta.url);
125
125
  const __dirname = path.dirname(__filename);
126
- let templateDir = path.resolve(__dirname, "../templates/nx");
126
+ let templateDir = path.resolve(__dirname, '../templates/nx');
127
127
  if (!fs.existsSync(templateDir)) {
128
- templateDir = path.resolve(__dirname, "../../templates/nx");
128
+ templateDir = path.resolve(__dirname, '../../templates/nx');
129
129
  }
130
130
  if (!fs.existsSync(templateDir)) {
131
- templateDir = path.resolve(__dirname, "../../../templates/nx");
131
+ templateDir = path.resolve(__dirname, '../../../templates/nx');
132
132
  }
133
- const sampleE2eSrc = path.join(templateDir, "packages/playground/e2e");
133
+ const sampleE2eSrc = path.join(templateDir, 'packages/playground/e2e');
134
134
  if (!fs.existsSync(sampleE2eSrc)) {
135
135
  p.cancel(chalk.red(`Template directory not found at: ${sampleE2eSrc}`));
136
136
  return;
137
137
  }
138
138
  // 3. Resolve CLI dependency version
139
- let cliVersion = "^1.0.65";
139
+ let cliVersion = '^1.0.65';
140
140
  try {
141
141
  let pDir = __dirname;
142
142
  while (pDir !== path.dirname(pDir)) {
143
- const cand = path.join(pDir, "package.json");
143
+ const cand = path.join(pDir, 'package.json');
144
144
  if (fs.existsSync(cand)) {
145
- const cliPkg = JSON.parse(fs.readFileSync(cand, "utf-8"));
146
- if (cliPkg.name === "@testspectra/cli" && cliPkg.version) {
145
+ const cliPkg = JSON.parse(fs.readFileSync(cand, 'utf-8'));
146
+ if (cliPkg.name === '@testspectra/cli' && cliPkg.version) {
147
147
  cliVersion = `^${cliPkg.version}`;
148
148
  break;
149
149
  }
@@ -152,10 +152,10 @@ export async function addCommand(moduleNameArg, options = {}) {
152
152
  }
153
153
  }
154
154
  catch { }
155
- const cliDepVersion = cliVersion.startsWith("^") ? cliVersion : `^${cliVersion}`;
155
+ const cliDepVersion = cliVersion.startsWith('^') ? cliVersion : `^${cliVersion}`;
156
156
  function copyRecursive(src, dest, replacements = {}) {
157
157
  const base = path.basename(src);
158
- if (base === "node_modules" || base === "dist" || base === ".testspectra" || base === ".git") {
158
+ if (base === 'node_modules' || base === 'dist' || base === '.testspectra' || base === '.git') {
159
159
  return;
160
160
  }
161
161
  const stat = fs.statSync(src);
@@ -172,23 +172,23 @@ export async function addCommand(moduleNameArg, options = {}) {
172
172
  if (fs.existsSync(dest) && !options.force) {
173
173
  return;
174
174
  }
175
- let content = fs.readFileSync(src, "utf-8");
175
+ let content = fs.readFileSync(src, 'utf-8');
176
176
  for (const [k, v] of Object.entries(replacements)) {
177
177
  content = content.replaceAll(k, v);
178
178
  }
179
- if (dest.endsWith("package.json")) {
179
+ if (dest.endsWith('package.json')) {
180
180
  try {
181
181
  const pkg = JSON.parse(content);
182
182
  if (pkg.devDependencies) {
183
183
  for (const key of Object.keys(pkg.devDependencies)) {
184
- if (key.startsWith("@testspectra/") || pkg.devDependencies[key] === "workspace:*") {
184
+ if (key.startsWith('@testspectra/') || pkg.devDependencies[key] === 'workspace:*') {
185
185
  pkg.devDependencies[key] = cliDepVersion;
186
186
  }
187
187
  }
188
188
  }
189
189
  if (pkg.dependencies) {
190
190
  for (const key of Object.keys(pkg.dependencies)) {
191
- if (key.startsWith("@testspectra/") || pkg.dependencies[key] === "workspace:*") {
191
+ if (key.startsWith('@testspectra/') || pkg.dependencies[key] === 'workspace:*') {
192
192
  pkg.dependencies[key] = cliDepVersion;
193
193
  }
194
194
  }
@@ -197,7 +197,7 @@ export async function addCommand(moduleNameArg, options = {}) {
197
197
  }
198
198
  catch { }
199
199
  }
200
- fs.writeFileSync(dest, content, "utf-8");
200
+ fs.writeFileSync(dest, content, 'utf-8');
201
201
  }
202
202
  }
203
203
  // If in a Monorepo
@@ -205,13 +205,13 @@ export async function addCommand(moduleNameArg, options = {}) {
205
205
  let pnpmPatterns = [];
206
206
  if (fs.existsSync(pnpmWorkspacePath)) {
207
207
  try {
208
- pnpmPatterns = parsePnpmWorkspaceGlobs(fs.readFileSync(pnpmWorkspacePath, "utf-8"));
208
+ pnpmPatterns = parsePnpmWorkspaceGlobs(fs.readFileSync(pnpmWorkspacePath, 'utf-8'));
209
209
  }
210
210
  catch { }
211
211
  }
212
212
  const detectedProjects = scanDirectoriesForProjects(rootDir, pnpmPatterns);
213
- let targetProjectPath = "";
214
- let featureName = "";
213
+ let targetProjectPath = '';
214
+ let featureName = '';
215
215
  if (moduleNameArg) {
216
216
  // Find if moduleNameArg matches an existing detected package
217
217
  const match = detectedProjects.find((p) => p.name === moduleNameArg || p.relPath === moduleNameArg || path.basename(p.relPath) === moduleNameArg);
@@ -226,35 +226,35 @@ export async function addCommand(moduleNameArg, options = {}) {
226
226
  }
227
227
  else {
228
228
  const mode = await p.select({
229
- message: "How would you like to add the TestSpectra module?",
229
+ message: 'How would you like to add the TestSpectra module?',
230
230
  options: [
231
231
  {
232
- label: "Add E2E to an existing workspace project / feature",
233
- value: "existing",
234
- hint: "Attach E2E test suite to a detected workspace package",
232
+ label: 'Add E2E to an existing workspace project / feature',
233
+ value: 'existing',
234
+ hint: 'Attach E2E test suite to a detected workspace package',
235
235
  },
236
236
  {
237
- label: "Create a new custom feature / module path",
238
- value: "custom",
239
- hint: "Specify a custom path e.g. packages/features/billing",
237
+ label: 'Create a new custom feature / module path',
238
+ value: 'custom',
239
+ hint: 'Specify a custom path e.g. packages/features/billing',
240
240
  },
241
241
  ],
242
242
  });
243
243
  if (p.isCancel(mode)) {
244
- p.cancel("Operation cancelled.");
244
+ p.cancel('Operation cancelled.');
245
245
  return;
246
246
  }
247
- if (mode === "existing" && detectedProjects.length > 0) {
247
+ if (mode === 'existing' && detectedProjects.length > 0) {
248
248
  const choice = await p.select({
249
- message: "Select workspace project to add TestSpectra E2E testing to:",
249
+ message: 'Select workspace project to add TestSpectra E2E testing to:',
250
250
  options: detectedProjects.map((proj) => ({
251
251
  value: proj.relPath,
252
252
  label: `${proj.relPath} (${proj.name})`,
253
- hint: proj.hasNxProject ? "Nx project.json" : "package.json",
253
+ hint: proj.hasNxProject ? 'Nx project.json' : 'package.json',
254
254
  })),
255
255
  });
256
256
  if (p.isCancel(choice)) {
257
- p.cancel("Operation cancelled.");
257
+ p.cancel('Operation cancelled.');
258
258
  return;
259
259
  }
260
260
  targetProjectPath = choice;
@@ -262,22 +262,22 @@ export async function addCommand(moduleNameArg, options = {}) {
262
262
  }
263
263
  else {
264
264
  const customPath = await p.text({
265
- message: "Enter path for the new feature module (e.g. packages/features/payment):",
266
- placeholder: "packages/features/new-feature",
265
+ message: 'Enter path for the new feature module (e.g. packages/features/payment):',
266
+ placeholder: 'packages/features/new-feature',
267
267
  validate: (val) => {
268
268
  if (!val || !val.trim())
269
- return "Path cannot be empty";
269
+ return 'Path cannot be empty';
270
270
  },
271
271
  });
272
272
  if (p.isCancel(customPath)) {
273
- p.cancel("Operation cancelled.");
273
+ p.cancel('Operation cancelled.');
274
274
  return;
275
275
  }
276
276
  targetProjectPath = customPath.trim();
277
277
  featureName = path.basename(targetProjectPath);
278
278
  }
279
279
  }
280
- const e2eFolderName = options.e2eFolder || "e2e";
280
+ const e2eFolderName = options.e2eFolder || 'e2e';
281
281
  const targetE2eDir = path.join(rootDir, targetProjectPath, e2eFolderName);
282
282
  const e2eProjectName = `${featureName}-${e2eFolderName}`;
283
283
  if (fs.existsSync(targetE2eDir) && !options.force) {
@@ -287,38 +287,39 @@ export async function addCommand(moduleNameArg, options = {}) {
287
287
  const s = p.spinner();
288
288
  s.start(`Scaffolding ${e2eProjectName} in ./${path.relative(rootDir, targetE2eDir)}...`);
289
289
  copyRecursive(sampleE2eSrc, targetE2eDir, {
290
- "playground-e2e": e2eProjectName,
291
- "packages/playground/e2e": path.relative(rootDir, targetE2eDir).replace(/\\/g, "/"),
292
- "scope:playground": `scope:${featureName}`,
290
+ 'playground-e2e': e2eProjectName,
291
+ 'packages/playground/e2e': path.relative(rootDir, targetE2eDir).replace(/\\/g, '/'),
292
+ 'scope:playground': `scope:${featureName}`,
293
293
  });
294
294
  // Adjust project.json
295
- const projJsonPath = path.join(targetE2eDir, "project.json");
295
+ const projJsonPath = path.join(targetE2eDir, 'project.json');
296
296
  if (fs.existsSync(projJsonPath)) {
297
297
  try {
298
- const pObj = JSON.parse(fs.readFileSync(projJsonPath, "utf-8"));
298
+ const pObj = JSON.parse(fs.readFileSync(projJsonPath, 'utf-8'));
299
299
  pObj.name = e2eProjectName;
300
- pObj.sourceRoot = path.relative(rootDir, targetE2eDir).replace(/\\/g, "/");
301
- pObj.targets.e2e.options.cwd = path.relative(rootDir, targetE2eDir).replace(/\\/g, "/");
302
- pObj.targets["type-check"].options.cwd = ".";
303
- pObj.tags = [`scope:${featureName}`, "type:e2e"];
304
- fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), "utf-8");
300
+ pObj.sourceRoot = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
301
+ pObj.targets.e2e.options.cwd = path.relative(rootDir, targetE2eDir).replace(/\\/g, '/');
302
+ pObj.targets['type-check'].options.cwd = '.';
303
+ pObj.tags = [`scope:${featureName}`, 'type:e2e'];
304
+ fs.writeFileSync(projJsonPath, JSON.stringify(pObj, null, 2), 'utf-8');
305
305
  }
306
306
  catch { }
307
307
  }
308
308
  // Ensure no local spectra.config.ts (centralized in root)
309
- const localConfig = path.join(targetE2eDir, "spectra.config.ts");
309
+ const localConfig = path.join(targetE2eDir, 'spectra.config.ts');
310
310
  if (fs.existsSync(localConfig))
311
311
  fs.unlinkSync(localConfig);
312
- // Ensure Root Solution tsconfig.json references tsconfig.spectra.json
313
- const rootTsConfigPath = path.join(rootDir, "tsconfig.json");
312
+ // Ensure Root Solution tsconfig.json references .testspectra/tsconfig.json
313
+ const rootTsConfigPath = path.join(rootDir, 'tsconfig.json');
314
314
  if (fs.existsSync(rootTsConfigPath)) {
315
315
  try {
316
- const rootTsConfig = JSON.parse(fs.readFileSync(rootTsConfigPath, "utf-8"));
316
+ const rootTsConfig = JSON.parse(fs.readFileSync(rootTsConfigPath, 'utf-8'));
317
317
  if (!rootTsConfig.references)
318
318
  rootTsConfig.references = [];
319
- if (!rootTsConfig.references.some((r) => r.path === "./tsconfig.spectra.json")) {
320
- rootTsConfig.references.push({ path: "./tsconfig.spectra.json" });
321
- fs.writeFileSync(rootTsConfigPath, JSON.stringify(rootTsConfig, null, 2), "utf-8");
319
+ rootTsConfig.references = rootTsConfig.references.filter((r) => r.path !== './tsconfig.spectra.json');
320
+ if (!rootTsConfig.references.some((r) => r.path === './.testspectra/tsconfig.json')) {
321
+ rootTsConfig.references.push({ path: './.testspectra/tsconfig.json' });
322
+ fs.writeFileSync(rootTsConfigPath, JSON.stringify(rootTsConfig, null, 2), 'utf-8');
322
323
  }
323
324
  }
324
325
  catch { }
@@ -329,38 +330,38 @@ export async function addCommand(moduleNameArg, options = {}) {
329
330
  p.log.success(chalk.green(`Directory: ./${path.relative(rootDir, targetE2eDir)}`));
330
331
  p.log.success(chalk.green(`Nx Target: ${e2eProjectName}:e2e`));
331
332
  p.log.message(chalk.cyan(`Run tests: pnpm nx run ${e2eProjectName}:e2e`));
332
- p.outro(chalk.bold.green("🎉 New TestSpectra module ready!"));
333
+ p.outro(chalk.bold.green('🎉 New TestSpectra module ready!'));
333
334
  return;
334
335
  }
335
336
  // Standalone mode: adding a new spec suite in specs/<ModuleName>
336
337
  let targetModule = moduleNameArg;
337
338
  if (!targetModule) {
338
339
  const input = await p.text({
339
- message: "Enter name of the new test suite/module (e.g. Payment, Profile):",
340
- placeholder: "Checkout",
340
+ message: 'Enter name of the new test suite/module (e.g. Payment, Profile):',
341
+ placeholder: 'Checkout',
341
342
  validate: (val) => {
342
343
  if (!val || !val.trim())
343
- return "Module name cannot be empty";
344
+ return 'Module name cannot be empty';
344
345
  },
345
346
  });
346
347
  if (p.isCancel(input)) {
347
- p.cancel("Operation cancelled.");
348
+ p.cancel('Operation cancelled.');
348
349
  return;
349
350
  }
350
351
  targetModule = input.trim();
351
352
  }
352
- const specDir = path.join(cwd, "specs", targetModule, "TC-01");
353
- const poDir = path.join(cwd, "page-objects", `${targetModule}Page`);
353
+ const specDir = path.join(cwd, 'specs', targetModule, 'TC-01');
354
+ const poDir = path.join(cwd, 'page-objects', `${targetModule}Page`);
354
355
  if (!fs.existsSync(specDir))
355
356
  fs.mkdirSync(specDir, { recursive: true });
356
357
  if (!fs.existsSync(poDir))
357
358
  fs.mkdirSync(poDir, { recursive: true });
358
359
  // Create starter page object
359
360
  const poContent = `class ${targetModule}Page {\n get mainTitle() {\n return $("h1");\n }\n\n async open() {\n await Spectra.navigate("/${targetModule.toLowerCase()}");\n }\n}\n\nexport default new ${targetModule}Page();\n`;
360
- fs.writeFileSync(path.join(poDir, "common.ts"), poContent, "utf-8");
361
+ fs.writeFileSync(path.join(poDir, 'common.ts'), poContent, 'utf-8');
361
362
  // Create starter test spec (Zero-import, flat it() block)
362
363
  const specContent = `it("should verify ${targetModule} page components", async () => {\n await ${targetModule}Page.open();\n await ${targetModule}Page.mainTitle.shouldBeVisible();\n});\n`;
363
- fs.writeFileSync(path.join(specDir, "web.test.ts"), specContent, "utf-8");
364
+ fs.writeFileSync(path.join(specDir, 'web.test.ts'), specContent, 'utf-8');
364
365
  TypeGenerator.writeDeclarationFiles(cwd);
365
366
  p.log.success(chalk.green(`Created Page Object: ./page-objects/${targetModule}Page/common.ts`));
366
367
  p.log.success(chalk.green(`Created Test Spec: ./specs/${targetModule}/TC-01/web.test.ts`));
@@ -0,0 +1,11 @@
1
+ export interface CloudRunOptions {
2
+ project: string;
3
+ projectName?: string;
4
+ branch?: string;
5
+ commit?: string;
6
+ platform?: string;
7
+ wait?: boolean;
8
+ timeout?: string;
9
+ }
10
+ export declare function parseTimeoutMs(timeout: string): number;
11
+ export declare function cloudRunCommand(options: CloudRunOptions): Promise<void>;
@@ -0,0 +1,118 @@
1
+ import { execFileSync } from 'child_process';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ const POLL_INTERVAL_MS = 5000;
5
+ const DEFAULT_TIMEOUT = '30m';
6
+ function detectGitRef(...args) {
7
+ try {
8
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ export function parseTimeoutMs(timeout) {
15
+ const match = /^(\d+)(s|m|h)?$/.exec(timeout.trim());
16
+ if (!match)
17
+ return 30 * 60 * 1000;
18
+ const value = Number(match[1]);
19
+ const unit = match[2] ?? 'm';
20
+ const multiplier = unit === 's' ? 1000 : unit === 'h' ? 3_600_000 : 60_000;
21
+ return value * multiplier;
22
+ }
23
+ // Same credential path as the trigger endpoint expects: a Gitea PAT, not the
24
+ // dashboard's OAuth2 session — a CI runner can't complete a browser redirect.
25
+ // See docs/v2/cloud/git-spec-standards-and-workflow.md.
26
+ function requireEnv(name) {
27
+ const value = process.env[name];
28
+ if (!value) {
29
+ console.error(chalk.red(`Missing required environment variable: ${name}`));
30
+ console.error(chalk.gray('Set it as a CI secret — see docs/v2/cloud/git-spec-standards-and-workflow.md'));
31
+ }
32
+ return value;
33
+ }
34
+ export async function cloudRunCommand(options) {
35
+ const spectraUrl = requireEnv('SPECTRA_URL')?.replace(/\/$/, '');
36
+ const spectraToken = requireEnv('SPECTRA_TOKEN');
37
+ if (!spectraUrl || !spectraToken) {
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const branch = options.branch ?? detectGitRef('rev-parse', '--abbrev-ref', 'HEAD');
42
+ if (!branch) {
43
+ console.error(chalk.red('Could not determine branch: pass --branch or run inside a git repository.'));
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+ const commit = options.commit ?? detectGitRef('rev-parse', 'HEAD');
48
+ const platform = options.platform ?? 'web';
49
+ const wait = options.wait !== false;
50
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
51
+ const spinner = ora(`Triggering cloud run for ${chalk.bold(options.project)}@${branch}...`).start();
52
+ let trigger;
53
+ try {
54
+ const res = await fetch(`${spectraUrl}/api/v1/runs/trigger`, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ Authorization: `Bearer ${spectraToken}`,
59
+ },
60
+ body: JSON.stringify({
61
+ project_id: options.project,
62
+ project_name: options.projectName ?? options.project,
63
+ branch,
64
+ commit_hash: commit,
65
+ platform,
66
+ }),
67
+ });
68
+ if (!res.ok) {
69
+ const body = await res.json().catch(() => ({ error: res.statusText }));
70
+ throw new Error(body.error ?? `HTTP ${res.status}`);
71
+ }
72
+ trigger = (await res.json());
73
+ }
74
+ catch (err) {
75
+ spinner.fail(chalk.red(`Failed to trigger cloud run: ${err.message}`));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ spinner.succeed(`Cloud run queued: ${chalk.cyan(trigger.job_id)}`);
80
+ if (!wait) {
81
+ console.log(chalk.gray(`Not waiting for result. Poll ${spectraUrl}/api/v1/jobs/${trigger.job_id}/status yourself.`));
82
+ return;
83
+ }
84
+ await waitForJob(spectraUrl, spectraToken, trigger.job_id, timeout);
85
+ }
86
+ async function waitForJob(spectraUrl, spectraToken, jobId, timeout) {
87
+ const spinner = ora('Waiting for run to finish...').start();
88
+ const deadline = Date.now() + parseTimeoutMs(timeout);
89
+ while (Date.now() < deadline) {
90
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
91
+ let job;
92
+ try {
93
+ const res = await fetch(`${spectraUrl}/api/v1/jobs/${jobId}/status`, {
94
+ headers: { Authorization: `Bearer ${spectraToken}` },
95
+ });
96
+ if (!res.ok)
97
+ throw new Error(`HTTP ${res.status}`);
98
+ job = (await res.json());
99
+ }
100
+ catch (err) {
101
+ spinner.text = chalk.yellow(`Poll failed, retrying: ${err.message}`);
102
+ continue;
103
+ }
104
+ if (job.status === 'done') {
105
+ spinner.succeed(chalk.green(`Run passed (run_id: ${job.run_id})`));
106
+ process.exitCode = 0;
107
+ return;
108
+ }
109
+ if (job.status === 'failed') {
110
+ spinner.fail(chalk.red(`Run failed (run_id: ${job.run_id})${job.error_log ? `: ${job.error_log}` : ''}`));
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ spinner.text = `Run status: ${job.status}`;
115
+ }
116
+ spinner.fail(chalk.red(`Timed out after ${timeout} waiting for the run to finish.`));
117
+ process.exitCode = 1;
118
+ }
@@ -1,21 +1,21 @@
1
- import chalk from "chalk";
2
- import * as p from "@clack/prompts";
3
- import { DemoServer } from "../utils/demo-server.js";
1
+ import chalk from 'chalk';
2
+ import * as p from '@clack/prompts';
3
+ import { DemoServer } from '../utils/demo-server.js';
4
4
  export async function demoCommand(options) {
5
5
  const port = options.port ? parseInt(options.port, 10) : 5173;
6
- p.intro(chalk.bold.cyan("🌐 TestSpectra Demo Web App"));
6
+ p.intro(chalk.bold.cyan('🌐 TestSpectra Demo Web App'));
7
7
  const server = new DemoServer(port);
8
8
  try {
9
9
  const { url } = await server.start();
10
10
  p.log.success(chalk.green(`Demo web app is live at: ${chalk.bold.underline(url)}`));
11
11
  p.log.info(chalk.dim("You can now run tests against this server in another terminal (e.g. 'spectra run -t web')."));
12
- p.log.info(chalk.dim("Press Ctrl+C to stop the server."));
12
+ p.log.info(chalk.dim('Press Ctrl+C to stop the server.'));
13
13
  // Handle termination gracefully
14
- process.on("SIGINT", async () => {
14
+ process.on('SIGINT', async () => {
15
15
  await server.stop();
16
16
  process.exit(0);
17
17
  });
18
- process.on("SIGTERM", async () => {
18
+ process.on('SIGTERM', async () => {
19
19
  await server.stop();
20
20
  process.exit(0);
21
21
  });
@@ -1,3 +1,15 @@
1
- export declare function devicesCommand(options: {
2
- target?: "android" | "ios" | "web" | "all";
1
+ export interface AdbDevice {
2
+ udid: string;
3
+ id: string;
4
+ model: string;
5
+ name: string;
6
+ status: string;
7
+ connectionType: 'usb' | 'wifi' | string;
8
+ version?: string | null;
9
+ androidId?: string | null;
10
+ }
11
+ export declare function getConnectedDevices(cwd?: string): Promise<AdbDevice[]>;
12
+ export declare function devicesCommand(options?: {
13
+ target?: 'android' | 'ios' | 'web' | 'all';
14
+ json?: boolean;
3
15
  }): Promise<void>;