@test-bro/cli 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +13 -14
  2. package/dist/index.js +409 -82
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -7,18 +7,17 @@ Command-line interface for AI-powered browser testing. Generate, run, and manage
7
7
  ### Global Installation (Recommended)
8
8
 
9
9
  ```bash
10
- npm install -g testbro-cli
10
+ npm install -g @test-bro/cli
11
11
  ```
12
12
 
13
13
  ### Local Development
14
14
 
15
15
  ```bash
16
- cd apps/cli
17
- pnpm install
18
- pnpm build
19
- pnpm link --global
16
+ npm install -g @test-bro/cli
20
17
  ```
21
18
 
19
+ For local CLI validation workflows (unit tests, dist smoke tests, and `npm pack` artifact testing), see [`local-cli-testing.md`](./local-cli-testing.md).
20
+
22
21
  ## Quick Start
23
22
 
24
23
  ### 1. Authenticate
@@ -143,12 +142,12 @@ Authenticate with TestBro API.
143
142
  testbro login --token tb_live_xxxxx
144
143
 
145
144
  # Custom API URL
146
- testbro login --token tb_live_xxxxx --url https://api.testbro.dev
145
+ testbro login --token tb_live_xxxxx --url https://testbro.dev
147
146
  ```
148
147
 
149
148
  **Options:**
150
149
  - `-t, --token <token>`: API token from your TestBro account
151
- - `-u, --url <url>`: API URL (default: http://localhost:3024)
150
+ - `-u, --url <url>`: API URL (default: https://testbro.dev)
152
151
 
153
152
  #### `testbro logout`
154
153
 
@@ -405,7 +404,7 @@ TestBro Configuration
405
404
 
406
405
  User Config (~/.testbro/config.json)
407
406
  Path: /Users/name/.testbro/config.json
408
- API URL: http://localhost:3024
407
+ API URL: https://testbro.dev
409
408
  Authenticated: Yes
410
409
  Active Project: proj_abc123
411
410
 
@@ -465,7 +464,7 @@ Stores user-level settings and authentication.
465
464
 
466
465
  ```json
467
466
  {
468
- "apiUrl": "http://localhost:3024",
467
+ "apiUrl": "https://testbro.dev",
469
468
  "token": "tb_live_xxxxx",
470
469
  "activeProjectId": "proj_abc123"
471
470
  }
@@ -530,7 +529,7 @@ TEST_USERNAME=testuser@example.com
530
529
  TEST_PASSWORD=SecureP@ssw0rd
531
530
 
532
531
  # API URL override
533
- TESTBRO_API_URL=https://api.testbro.dev
532
+ TESTBRO_API_URL=https://testbro.dev
534
533
  ```
535
534
 
536
535
  ## Session Continuity
@@ -686,7 +685,7 @@ jobs:
686
685
  node-version: 18
687
686
 
688
687
  - name: Install TestBro CLI
689
- run: npm install -g testbro-cli
688
+ run: npm install -g @test-bro/cli
690
689
 
691
690
  - name: Run Tests
692
691
  env:
@@ -708,7 +707,7 @@ jobs:
708
707
  test:
709
708
  image: node:18
710
709
  script:
711
- - npm install -g testbro-cli
710
+ - npm install -g @test-bro/cli
712
711
  - testbro run --project $PROJECT_ID --format json > results.json
713
712
  artifacts:
714
713
  paths:
@@ -830,8 +829,8 @@ pnpm install
830
829
  # Build
831
830
  pnpm build
832
831
 
833
- # Link globally for testing
834
- pnpm link --global
832
+ # Install official CLI globally
833
+ npm install -g @test-bro/cli
835
834
 
836
835
  # Run locally
837
836
  pnpm start -- run --help
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -181,7 +180,7 @@ var init_config = __esm({
181
180
  "src/config.ts"() {
182
181
  "use strict";
183
182
  DEFAULT_CONFIG = {
184
- apiUrl: "https://www.testbro.dev"
183
+ apiUrl: "https://testbro.dev"
185
184
  };
186
185
  }
187
186
  });
@@ -339,7 +338,7 @@ function mergeWithCliOptions(projectConfig, cliOptions) {
339
338
  return merged;
340
339
  }
341
340
  function getProjectConfigSummary() {
342
- const { config, path: path9, error } = loadProjectConfig();
341
+ const { config, path: path10, error } = loadProjectConfig();
343
342
  let tokenSource = null;
344
343
  let token = null;
345
344
  if (process.env.TEST_BRO_TOKEN) {
@@ -357,7 +356,7 @@ function getProjectConfigSummary() {
357
356
  }
358
357
  }
359
358
  return {
360
- projectConfigPath: path9,
359
+ projectConfigPath: path10,
361
360
  projectConfig: config,
362
361
  error,
363
362
  token,
@@ -393,7 +392,8 @@ var init_project_config = __esm({
393
392
  steps: z.array(authStepSchema),
394
393
  credentials: z.record(z.string(), z.string()).optional(),
395
394
  storageStatePath: z.string().optional(),
396
- maxAge: z.number().optional()
395
+ maxAge: z.number().optional(),
396
+ waitForUrl: z.string().optional()
397
397
  });
398
398
  projectConfigSchema = z.object({
399
399
  $schema: z.string().optional(),
@@ -439,7 +439,7 @@ var init_project_config = __esm({
439
439
  // src/index.ts
440
440
  import "dotenv/config";
441
441
  import { Command } from "commander";
442
- import chalk28 from "chalk";
442
+ import chalk29 from "chalk";
443
443
 
444
444
  // src/commands/login.ts
445
445
  import chalk3 from "chalk";
@@ -616,6 +616,7 @@ async function apiRequestCore(endpoint, options = {}, authToken, requireAuth = t
616
616
  };
617
617
  if (token) {
618
618
  headers.Authorization = `Bearer ${token}`;
619
+ headers["x-api-key"] = token;
619
620
  }
620
621
  const response = await fetch(url, {
621
622
  ...options,
@@ -816,40 +817,50 @@ async function uploadArtifacts(runId, options) {
816
817
  }
817
818
  const token = getToken();
818
819
  if (!token) {
819
- return {};
820
+ throw new Error("No API token found for artifact upload");
820
821
  }
821
- try {
822
- const apiUrl = getApiUrl();
823
- const url = `${apiUrl}/api/runs/${runId}/artifacts`;
824
- const formData = new FormData();
825
- if (videoPath) {
826
- const videoBuffer = fs2.readFileSync(videoPath);
827
- const videoType = videoPath.endsWith(".mp4") ? "video/mp4" : "video/webm";
828
- const videoBlob = new Blob([videoBuffer], { type: videoType });
829
- const videoFileName = videoPath.split("/").pop() || "video.webm";
830
- formData.append("video", videoBlob, videoFileName);
831
- }
832
- if (tracePath) {
833
- const traceBuffer = fs2.readFileSync(tracePath);
834
- const traceBlob = new Blob([traceBuffer], { type: "application/zip" });
835
- const traceFileName = tracePath.split("/").pop() || "trace.zip";
836
- formData.append("trace", traceBlob, traceFileName);
837
- }
838
- const response = await fetch(url, {
839
- method: "POST",
840
- headers: {
841
- Authorization: `Bearer ${token}`
842
- },
843
- body: formData
844
- });
845
- if (!response.ok) {
846
- return {};
822
+ const apiUrl = getApiUrl();
823
+ const url = `${apiUrl}/api/runs/${runId}/artifacts`;
824
+ const formData = new FormData();
825
+ if (videoPath) {
826
+ const videoBuffer = fs2.readFileSync(videoPath);
827
+ const videoType = videoPath.endsWith(".mp4") ? "video/mp4" : "video/webm";
828
+ const videoBlob = new Blob([videoBuffer], { type: videoType });
829
+ const videoFileName = videoPath.split("/").pop() || "video.webm";
830
+ formData.append("video", videoBlob, videoFileName);
831
+ }
832
+ if (tracePath) {
833
+ const traceBuffer = fs2.readFileSync(tracePath);
834
+ const traceBlob = new Blob([traceBuffer], { type: "application/zip" });
835
+ const traceFileName = tracePath.split("/").pop() || "trace.zip";
836
+ formData.append("trace", traceBlob, traceFileName);
837
+ }
838
+ const response = await fetch(url, {
839
+ method: "POST",
840
+ headers: {
841
+ Authorization: `Bearer ${token}`
842
+ },
843
+ body: formData
844
+ });
845
+ if (!response.ok) {
846
+ let details = "";
847
+ try {
848
+ const contentType = response.headers.get("content-type") || "";
849
+ if (contentType.includes("application/json")) {
850
+ const errData = await response.json();
851
+ details = errData.error || JSON.stringify(errData.details || errData.data || "");
852
+ } else {
853
+ details = await response.text();
854
+ }
855
+ } catch {
856
+ details = "";
847
857
  }
848
- const data = await response.json();
849
- return data.data || {};
850
- } catch {
851
- return {};
858
+ throw new Error(
859
+ `Artifact upload failed (${response.status}${response.statusText ? ` ${response.statusText}` : ""})${details ? `: ${details}` : ""}`
860
+ );
852
861
  }
862
+ const data = await response.json();
863
+ return data.data || {};
853
864
  }
854
865
  async function listFeatures(projectId) {
855
866
  return apiRequest(`/api/projects/${projectId}/features`);
@@ -1034,10 +1045,10 @@ function extractValidationHints(details) {
1034
1045
  for (const issue of obj.issues) {
1035
1046
  if (typeof issue === "object" && issue !== null) {
1036
1047
  const i = issue;
1037
- const path9 = Array.isArray(i.path) ? i.path.join(".") : String(i.path ?? "");
1048
+ const path10 = Array.isArray(i.path) ? i.path.join(".") : String(i.path ?? "");
1038
1049
  const message = typeof i.message === "string" ? i.message : "";
1039
- if (path9 && message) {
1040
- hints.push(`${path9}: ${message}`);
1050
+ if (path10 && message) {
1051
+ hints.push(`${path10}: ${message}`);
1041
1052
  } else if (message) {
1042
1053
  hints.push(message);
1043
1054
  }
@@ -1419,7 +1430,7 @@ async function quickstartCommand(options = {}) {
1419
1430
  console.log("");
1420
1431
  console.log(chalk5.yellow(" Please authenticate first:"));
1421
1432
  console.log("");
1422
- console.log(chalk5.dim(" 1. Go to ") + chalk5.cyan("http://localhost:3024/dashboard/settings"));
1433
+ console.log(chalk5.dim(" 1. Go to ") + chalk5.cyan("https://testbro.dev/dashboard/settings"));
1423
1434
  console.log(chalk5.dim(" 2. Create an API token"));
1424
1435
  console.log(chalk5.dim(" 3. Run: ") + chalk5.cyan("testbro init") + chalk5.dim(" (or testbro login --token <your-token>)"));
1425
1436
  console.log("");
@@ -1434,7 +1445,7 @@ async function quickstartCommand(options = {}) {
1434
1445
  spinner.fail("Token is invalid or expired");
1435
1446
  console.log("");
1436
1447
  console.log(chalk5.yellow(" Please re-authenticate:"));
1437
- console.log(chalk5.dim(" 1. Go to ") + chalk5.cyan("http://localhost:3024/dashboard/settings"));
1448
+ console.log(chalk5.dim(" 1. Go to ") + chalk5.cyan("https://testbro.dev/dashboard/settings"));
1438
1449
  console.log(chalk5.dim(" 2. Create a new API token"));
1439
1450
  console.log(chalk5.dim(" 3. Run: ") + chalk5.cyan("testbro init") + chalk5.dim(" (or testbro login --token <your-token>)"));
1440
1451
  console.log("");
@@ -1875,7 +1886,7 @@ var AuthExecutor = class _AuthExecutor {
1875
1886
  );
1876
1887
  await _AuthExecutor.executeStep(page, resolvedStep);
1877
1888
  }
1878
- await page.waitForTimeout(2e3);
1889
+ await _AuthExecutor.waitForAuthComplete(page, authConfig, loginUrl);
1879
1890
  await context.storageState({ path: statePath });
1880
1891
  return statePath;
1881
1892
  } finally {
@@ -2034,6 +2045,42 @@ var AuthExecutor = class _AuthExecutor {
2034
2045
  }
2035
2046
  }
2036
2047
  }
2048
+ /**
2049
+ * Wait for the authentication flow to fully complete after all steps.
2050
+ *
2051
+ * Handles redirect-based auth flows (e.g., NextAuth v5 server actions)
2052
+ * where the session cookie is set during a redirect chain, not on the
2053
+ * initial response. Uses three strategies in order:
2054
+ *
2055
+ * 1. If `waitForUrl` is configured, wait for that specific URL pattern
2056
+ * 2. Otherwise, detect URL change from the login page (redirect completed)
2057
+ * 3. Wait for network idle to ensure all Set-Cookie headers are processed
2058
+ */
2059
+ static async waitForAuthComplete(page, authConfig, loginUrl) {
2060
+ const waitTimeout = 15e3;
2061
+ if (authConfig.waitForUrl) {
2062
+ try {
2063
+ await page.waitForURL(authConfig.waitForUrl, {
2064
+ timeout: waitTimeout,
2065
+ waitUntil: "load"
2066
+ });
2067
+ } catch {
2068
+ }
2069
+ } else {
2070
+ try {
2071
+ await page.waitForURL(
2072
+ (url) => url.toString() !== loginUrl,
2073
+ { timeout: waitTimeout, waitUntil: "load" }
2074
+ );
2075
+ } catch {
2076
+ }
2077
+ }
2078
+ try {
2079
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
2080
+ } catch {
2081
+ }
2082
+ await page.waitForTimeout(1e3);
2083
+ }
2037
2084
  /**
2038
2085
  * Delete the cached storage state file.
2039
2086
  * @param statePath - Path to the storage state file
@@ -3434,6 +3481,17 @@ You are a GOAL-ORIENTED test agent. Each step describes an INTENT, not a rigid i
3434
3481
  - Always explain your reasoning when deviating from the original step description
3435
3482
  - If an element is below the fold, suggest scrolling first
3436
3483
 
3484
+ ## Intent Safety Guardrails
3485
+ - NEVER click or fill a semantically unrelated element just to make progress.
3486
+ - If the requested target is not present, do NOT guess a random alternative action.
3487
+ - Preserve intent class when adapting. Examples:
3488
+ - form input intent -> editable field
3489
+ - navigation intent -> relevant nav control/link
3490
+ - confirmation intent -> intended submit/confirm action
3491
+ - Prefer this fallback order when target is missing: **scroll -> wait -> navigate/click a clearly relevant entry point for the SAME intent class**.
3492
+ - Before acting, validate evidence from Available Elements and visible UI cues (role, label, testId, text). If evidence is weak or ambiguous, lower confidence or choose wait/scroll instead of a risky click.
3493
+ - If no relevant action is possible on the current page, return confidence 0 and explain why.
3494
+
3437
3495
  Always respond with valid JSON only. Do not include any other text.`;
3438
3496
  var MAX_MEMORY_ENTRIES = 10;
3439
3497
  function formatStepMemory(memory) {
@@ -3497,7 +3555,9 @@ function generateFillPrompt(fieldDescription, textToEnter, availableElements, me
3497
3555
  Then type: "${textToEnter}"`,
3498
3556
  `IMPORTANT: For fill actions, you MUST target an <input>, <textarea>, or <select> element \u2014 NOT a link, button, div, or card.
3499
3557
  If a dialog or modal is open, look for input fields INSIDE the dialog, not behind it.
3500
- Identify the input field and provide coordinates to click it before typing. Look for data-testid, name, or placeholder attributes for the best selector.`,
3558
+ Identify the input field and provide coordinates to click it before typing. Look for data-testid, name, or placeholder attributes for the best selector.
3559
+ If no editable field for this intent is visible, do NOT fill a non-input element.
3560
+ If input is unavailable in current state, choose a preparatory step that keeps the same intent class (e.g., open relevant form/screen), otherwise return confidence 0.`,
3501
3561
  availableElements,
3502
3562
  memoryContext
3503
3563
  );
@@ -3571,6 +3631,9 @@ Consider:
3571
3631
  - You may need to scroll to find the element
3572
3632
  - A modal or overlay may be blocking \u2014 close it first
3573
3633
  - The page may be in a different state than expected \u2014 adapt accordingly
3634
+ - Preserve intent class when adapting (input -> input, navigation -> navigation, assertion -> evidence)
3635
+ - Prefer preparatory actions (scroll/wait/open relevant surface) before risky substitutions
3636
+ - NEVER use an unrelated element as a substitute action
3574
3637
  - If the intent is truly impossible on this page, set confidence to 0
3575
3638
 
3576
3639
  Respond with the action you would take to achieve the intent.`
@@ -8349,28 +8412,42 @@ async function runTestExecution(options) {
8349
8412
  if (isPretty) {
8350
8413
  if (uploadResult.videoUrl) {
8351
8414
  log.plain(`${chalk16.bold("Video URL:")} ${chalk16.cyan(uploadResult.videoUrl)}`);
8415
+ } else if (videoPath) {
8416
+ log.warn("Video upload completed without a Video URL. Keeping local file.");
8417
+ log.dim(`Local video: ${videoPath}`);
8352
8418
  }
8353
8419
  if (uploadResult.traceUrl) {
8354
8420
  log.plain(`${chalk16.bold("Trace URL:")} ${chalk16.cyan(uploadResult.traceUrl)}`);
8421
+ } else if (tracePath) {
8422
+ log.warn("Trace upload completed without a Trace URL. Keeping local file.");
8423
+ log.dim(`Local trace: ${tracePath}`);
8355
8424
  }
8356
8425
  }
8357
- if (videoPath) {
8426
+ if (videoPath && uploadResult.videoUrl) {
8358
8427
  try {
8359
8428
  fs7.unlinkSync(videoPath);
8360
8429
  fs7.rmSync(path7.dirname(videoPath), { recursive: true, force: true });
8361
8430
  } catch {
8362
8431
  }
8363
8432
  }
8364
- if (tracePath) {
8433
+ if (tracePath && uploadResult.traceUrl) {
8365
8434
  try {
8366
8435
  fs7.unlinkSync(tracePath);
8367
8436
  fs7.rmSync(path7.dirname(tracePath), { recursive: true, force: true });
8368
8437
  } catch {
8369
8438
  }
8370
8439
  }
8371
- } catch {
8440
+ } catch (error) {
8372
8441
  if (isPretty) {
8373
- log.dim(" Warning: Failed to upload recordings");
8442
+ const message = error instanceof Error ? error.message : String(error);
8443
+ log.warn("Failed to upload recordings");
8444
+ log.dim(`Reason: ${message}`);
8445
+ if (videoPath) {
8446
+ log.dim(`Local video kept: ${videoPath}`);
8447
+ }
8448
+ if (tracePath) {
8449
+ log.dim(`Local trace kept: ${tracePath}`);
8450
+ }
8374
8451
  }
8375
8452
  }
8376
8453
  }
@@ -9879,10 +9956,245 @@ function formatStatus2(status) {
9879
9956
  }
9880
9957
  }
9881
9958
 
9959
+ // src/commands/upgrade.ts
9960
+ import { execSync } from "child_process";
9961
+ import chalk28 from "chalk";
9962
+
9963
+ // package.json
9964
+ var package_default = {
9965
+ name: "@test-bro/cli",
9966
+ publishConfig: {
9967
+ access: "public"
9968
+ },
9969
+ version: "0.1.1",
9970
+ description: "TestBro CLI - AI-powered browser testing from your terminal",
9971
+ type: "module",
9972
+ bin: {
9973
+ testbro: "dist/index.js"
9974
+ },
9975
+ files: [
9976
+ "dist"
9977
+ ],
9978
+ engines: {
9979
+ node: ">=18"
9980
+ },
9981
+ keywords: [
9982
+ "testing",
9983
+ "browser-testing",
9984
+ "playwright",
9985
+ "ai-testing",
9986
+ "test-automation",
9987
+ "e2e",
9988
+ "qa"
9989
+ ],
9990
+ repository: {
9991
+ type: "git",
9992
+ url: "git+https://github.com/chernobelenkiy/test-bro.git",
9993
+ directory: "apps/cli"
9994
+ },
9995
+ license: "MIT",
9996
+ scripts: {
9997
+ build: "tsup",
9998
+ dev: "tsup --watch",
9999
+ typecheck: "tsc --noEmit",
10000
+ lint: "eslint src/",
10001
+ test: "vitest run",
10002
+ "test:watch": "vitest",
10003
+ "test:coverage": "vitest run --coverage"
10004
+ },
10005
+ dependencies: {
10006
+ commander: "^12.1.0",
10007
+ chalk: "^5.3.0",
10008
+ ora: "^8.1.1",
10009
+ dotenv: "^16.4.7",
10010
+ playwright: "^1.40.0",
10011
+ zod: "^3.24.1"
10012
+ },
10013
+ devDependencies: {
10014
+ "@testbro/core": "workspace:*",
10015
+ "@testbro/runner": "workspace:*",
10016
+ "@testbro/ai-agent": "workspace:*",
10017
+ "@types/node": "^22.10.2",
10018
+ "@vitest/coverage-v8": "^2.1.0",
10019
+ tsup: "^8.3.5",
10020
+ typescript: "^5.7.2",
10021
+ vitest: "^2.1.0"
10022
+ }
10023
+ };
10024
+
10025
+ // src/version.ts
10026
+ var CLI_VERSION = package_default.version;
10027
+
10028
+ // src/version-check.ts
10029
+ init_config();
10030
+ import * as fs10 from "fs";
10031
+ import * as path9 from "path";
10032
+ var CACHE_FILE_NAME = "version-check.json";
10033
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
10034
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@test-bro/cli/latest";
10035
+ var FETCH_TIMEOUT_MS = 5e3;
10036
+ function getCachePath() {
10037
+ return path9.join(getConfigDir(), CACHE_FILE_NAME);
10038
+ }
10039
+ function readCache() {
10040
+ try {
10041
+ const cachePath = getCachePath();
10042
+ if (!fs10.existsSync(cachePath)) {
10043
+ return null;
10044
+ }
10045
+ const raw = fs10.readFileSync(cachePath, "utf-8");
10046
+ const data = JSON.parse(raw);
10047
+ const age = Date.now() - new Date(data.checkedAt).getTime();
10048
+ if (age > CACHE_TTL_MS) {
10049
+ return null;
10050
+ }
10051
+ return data;
10052
+ } catch {
10053
+ return null;
10054
+ }
10055
+ }
10056
+ function writeCache(latestVersion) {
10057
+ try {
10058
+ const configDir = getConfigDir();
10059
+ if (!fs10.existsSync(configDir)) {
10060
+ fs10.mkdirSync(configDir, { recursive: true, mode: 448 });
10061
+ }
10062
+ const data = {
10063
+ latestVersion,
10064
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
10065
+ };
10066
+ fs10.writeFileSync(getCachePath(), JSON.stringify(data, null, 2), {
10067
+ encoding: "utf-8",
10068
+ mode: 384
10069
+ });
10070
+ } catch {
10071
+ }
10072
+ }
10073
+ async function fetchLatestVersion() {
10074
+ try {
10075
+ const controller = new AbortController();
10076
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
10077
+ const response = await fetch(NPM_REGISTRY_URL, {
10078
+ signal: controller.signal,
10079
+ headers: { Accept: "application/json" }
10080
+ });
10081
+ clearTimeout(timeout);
10082
+ if (!response.ok) {
10083
+ return null;
10084
+ }
10085
+ const data = await response.json();
10086
+ return data.version ?? null;
10087
+ } catch {
10088
+ return null;
10089
+ }
10090
+ }
10091
+ function compareSemver(a, b) {
10092
+ const pa = a.split(".").map(Number);
10093
+ const pb = b.split(".").map(Number);
10094
+ const len = Math.max(pa.length, pb.length);
10095
+ for (let i = 0; i < len; i++) {
10096
+ const na = pa[i] ?? 0;
10097
+ const nb = pb[i] ?? 0;
10098
+ if (na < nb) return -1;
10099
+ if (na > nb) return 1;
10100
+ }
10101
+ return 0;
10102
+ }
10103
+ async function checkForUpdate(currentVersion) {
10104
+ const cached = readCache();
10105
+ if (cached) {
10106
+ if (compareSemver(currentVersion, cached.latestVersion) < 0) {
10107
+ return cached.latestVersion;
10108
+ }
10109
+ return null;
10110
+ }
10111
+ const latest = await fetchLatestVersion();
10112
+ if (!latest) {
10113
+ return null;
10114
+ }
10115
+ writeCache(latest);
10116
+ if (compareSemver(currentVersion, latest) < 0) {
10117
+ return latest;
10118
+ }
10119
+ return null;
10120
+ }
10121
+ function startVersionCheck(currentVersion) {
10122
+ const checkPromise = checkForUpdate(currentVersion).catch(() => null);
10123
+ return async () => {
10124
+ try {
10125
+ const latestVersion = await checkPromise;
10126
+ if (latestVersion) {
10127
+ const chalk30 = (await import("chalk")).default;
10128
+ console.log(
10129
+ chalk30.yellow(
10130
+ `
10131
+ Update available: ${currentVersion} \u2192 ${latestVersion}. Run \`testbro upgrade\` to update.`
10132
+ )
10133
+ );
10134
+ }
10135
+ } catch {
10136
+ }
10137
+ };
10138
+ }
10139
+
10140
+ // src/commands/upgrade.ts
10141
+ async function upgradeCommand() {
10142
+ console.log(chalk28.bold("TestBro CLI Upgrade"));
10143
+ console.log("");
10144
+ console.log(` Current version: ${chalk28.dim(CLI_VERSION)}`);
10145
+ console.log(chalk28.dim(" Checking npm registry..."));
10146
+ const latestVersion = await fetchLatestVersion();
10147
+ if (!latestVersion) {
10148
+ console.log(
10149
+ chalk28.red(
10150
+ " Failed to check npm registry. Please check your internet connection."
10151
+ )
10152
+ );
10153
+ process.exit(1);
10154
+ }
10155
+ console.log(` Latest version: ${chalk28.green(latestVersion)}`);
10156
+ console.log("");
10157
+ if (compareSemver(CLI_VERSION, latestVersion) >= 0) {
10158
+ console.log(chalk28.green(" You are already on the latest version."));
10159
+ return;
10160
+ }
10161
+ console.log(
10162
+ chalk28.yellow(
10163
+ ` Upgrading ${CLI_VERSION} \u2192 ${latestVersion}...`
10164
+ )
10165
+ );
10166
+ console.log("");
10167
+ try {
10168
+ execSync("npm install -g @test-bro/cli@latest", {
10169
+ stdio: "inherit"
10170
+ });
10171
+ writeCache(latestVersion);
10172
+ console.log("");
10173
+ console.log(
10174
+ chalk28.green(
10175
+ ` Successfully upgraded to @test-bro/cli@${latestVersion}`
10176
+ )
10177
+ );
10178
+ } catch {
10179
+ console.log("");
10180
+ console.log(
10181
+ chalk28.red(
10182
+ " Upgrade failed. You may need to run with sudo or fix npm permissions."
10183
+ )
10184
+ );
10185
+ console.log(
10186
+ chalk28.dim(
10187
+ " Try: sudo npm install -g @test-bro/cli@latest"
10188
+ )
10189
+ );
10190
+ process.exit(1);
10191
+ }
10192
+ }
10193
+
9882
10194
  // src/index.ts
9883
10195
  var program = new Command();
9884
- program.name("testbro").description("AI-powered browser testing from your terminal").version("0.1.0");
9885
- program.command("login").description("Authenticate with TestBro using an API token").argument("[email]", "Your email address (deprecated - use --token instead)").option("-t, --token <token>", "API token for authentication").option("-u, --url <url>", "API URL (default: http://localhost:3024)").action(async (email, options) => {
10196
+ program.name("testbro").description("AI-powered browser testing from your terminal").version(CLI_VERSION);
10197
+ program.command("login").description("Authenticate with TestBro using an API token").argument("[email]", "Your email address (deprecated - use --token instead)").option("-t, --token <token>", "API token for authentication").option("-u, --url <url>", "API URL (default: https://testbro.dev)").action(async (email, options) => {
9886
10198
  try {
9887
10199
  await loginCommand(email, { token: options.token, url: options.url });
9888
10200
  } catch (error) {
@@ -9985,7 +10297,7 @@ program.command("generate").description("Generate test cases from a file or desc
9985
10297
  program.command("run").description("Run tests against a URL").option("-u, --url <url>", "Target URL to test (or use baseUrl from test-bro.config.json)").option("--file <path>", "Path to file (AC/PRD) to generate and run tests from").option("-d, --description <text>", "Feature description to test").option("-p, --project <id>", "Project ID to use test cases from (overrides active)").option("-t, --test-case <id>", "Run specific test case only").option("-m, --mode <mode>", "Execution mode: selector (default), ai, or hybrid", "selector").option("--headed", "Run browser in headed mode (visible)", false).option("--remote", "Run tests on remote staging environment", false).option("-f, --format <format>", "Output format: pretty or json", "pretty").option("-v, --verbose", "Show verbose output", false).option("--timeout <ms>", "Timeout per step in milliseconds", "30000").option("--learn-selectors", "Learn selectors from AI actions", false).option("--yes", "Auto-confirm selector learning", false).option("--no-dedup", "Skip deduplication check when using --file").option("--auto-merge", "Auto-apply smart merge without prompting when using --file").option("--record-video", "Record video of the test execution", false).option("--record-trace", "Capture Playwright trace for debugging", false).option("-e, --env <name>", "Use a named environment (fetches baseUrl from API)").option("--credential <name>", "Use a named credential from the vault").option("--no-sequential", "Run test cases independently (re-navigate for each)").action(async (options) => {
9986
10298
  if (options.url && !validateUrl(options.url)) {
9987
10299
  displayError("Invalid URL format", `Provided URL: ${options.url}`);
9988
- console.log(chalk28.dim(' Example: testbro run --url https://example.com --description "..."'));
10300
+ console.log(chalk29.dim(' Example: testbro run --url https://example.com --description "..."'));
9989
10301
  process.exit(1);
9990
10302
  }
9991
10303
  if (options.description) {
@@ -10231,6 +10543,17 @@ releasesCmd.command("unpublish").description("Unpublish a release note (revert t
10231
10543
  process.exit(1);
10232
10544
  }
10233
10545
  });
10546
+ program.command("upgrade").description("Upgrade TestBro CLI to the latest version").action(async () => {
10547
+ try {
10548
+ await upgradeCommand();
10549
+ } catch (error) {
10550
+ if (error instanceof Error) {
10551
+ displayError(error.message);
10552
+ }
10553
+ printErrorHints(error);
10554
+ process.exit(1);
10555
+ }
10556
+ });
10234
10557
  var configCmd = program.command("config").description("Show or update configuration");
10235
10558
  function displayConfig() {
10236
10559
  const { getConfigSummary: getConfigSummary2 } = (init_config(), __toCommonJS(config_exports));
@@ -10241,72 +10564,72 @@ function displayConfig() {
10241
10564
  const userConfig = getConfigSummary2();
10242
10565
  const projectConfigInfo = getProjectConfigSummary2();
10243
10566
  console.log("");
10244
- console.log(chalk28.bold("TestBro Configuration"));
10567
+ console.log(chalk29.bold("TestBro Configuration"));
10245
10568
  console.log("");
10246
- console.log(chalk28.underline("User Config") + chalk28.dim(` (~/.testbro/config.json)`));
10247
- console.log(` ${chalk28.dim("Path:")} ${userConfig.configPath}`);
10248
- console.log(` ${chalk28.dim("API URL:")} ${userConfig.apiUrl}`);
10569
+ console.log(chalk29.underline("User Config") + chalk29.dim(` (~/.testbro/config.json)`));
10570
+ console.log(` ${chalk29.dim("Path:")} ${userConfig.configPath}`);
10571
+ console.log(` ${chalk29.dim("API URL:")} ${userConfig.apiUrl}`);
10249
10572
  console.log(
10250
- ` ${chalk28.dim("Authenticated:")} ${userConfig.authenticated ? chalk28.green("Yes") : chalk28.yellow("No")}`
10573
+ ` ${chalk29.dim("Authenticated:")} ${userConfig.authenticated ? chalk29.green("Yes") : chalk29.yellow("No")}`
10251
10574
  );
10252
10575
  if (userConfig.activeProjectId) {
10253
- console.log(` ${chalk28.dim("Active Project:")} ${userConfig.activeProjectId}`);
10576
+ console.log(` ${chalk29.dim("Active Project:")} ${userConfig.activeProjectId}`);
10254
10577
  }
10255
10578
  console.log("");
10256
- console.log(chalk28.underline("Project Config") + chalk28.dim(` (test-bro.config.json)`));
10579
+ console.log(chalk29.underline("Project Config") + chalk29.dim(` (test-bro.config.json)`));
10257
10580
  if (projectConfigInfo.projectConfigPath) {
10258
- console.log(` ${chalk28.dim("Path:")} ${projectConfigInfo.projectConfigPath}`);
10581
+ console.log(` ${chalk29.dim("Path:")} ${projectConfigInfo.projectConfigPath}`);
10259
10582
  if (projectConfigInfo.error) {
10260
- console.log(` ${chalk28.red("Error:")} ${projectConfigInfo.error}`);
10583
+ console.log(` ${chalk29.red("Error:")} ${projectConfigInfo.error}`);
10261
10584
  } else if (projectConfigInfo.projectConfig) {
10262
10585
  const pc = projectConfigInfo.projectConfig;
10263
10586
  if (pc.baseUrl) {
10264
- console.log(` ${chalk28.dim("Base URL:")} ${pc.baseUrl}`);
10587
+ console.log(` ${chalk29.dim("Base URL:")} ${pc.baseUrl}`);
10265
10588
  }
10266
10589
  if (pc.environment) {
10267
- console.log(` ${chalk28.dim("Environment:")} ${pc.environment}`);
10590
+ console.log(` ${chalk29.dim("Environment:")} ${pc.environment}`);
10268
10591
  }
10269
10592
  if (pc.mode) {
10270
- console.log(` ${chalk28.dim("Mode:")} ${pc.mode}`);
10593
+ console.log(` ${chalk29.dim("Mode:")} ${pc.mode}`);
10271
10594
  }
10272
10595
  if (pc.timeout) {
10273
- console.log(` ${chalk28.dim("Timeout:")} ${pc.timeout}ms`);
10596
+ console.log(` ${chalk29.dim("Timeout:")} ${pc.timeout}ms`);
10274
10597
  }
10275
10598
  if (pc.headed !== void 0) {
10276
- console.log(` ${chalk28.dim("Headed:")} ${pc.headed}`);
10599
+ console.log(` ${chalk29.dim("Headed:")} ${pc.headed}`);
10277
10600
  }
10278
10601
  if (pc.projectId) {
10279
- console.log(` ${chalk28.dim("Project ID:")} ${pc.projectId}`);
10602
+ console.log(` ${chalk29.dim("Project ID:")} ${pc.projectId}`);
10280
10603
  }
10281
10604
  const credNames = getCredentialNames2(pc);
10282
10605
  if (credNames.length > 0) {
10283
- console.log(` ${chalk28.dim("Credentials:")} ${credNames.join(", ")}`);
10606
+ console.log(` ${chalk29.dim("Credentials:")} ${credNames.join(", ")}`);
10284
10607
  }
10285
10608
  if (pc.deduplication) {
10286
10609
  const dedupEnabled = pc.deduplication.enabled !== false;
10287
10610
  const autoMerge = pc.deduplication.autoMerge === true;
10288
- console.log(` ${chalk28.dim("Deduplication:")} ${dedupEnabled ? chalk28.green("enabled") : chalk28.yellow("disabled")}${autoMerge ? chalk28.dim(" (auto-merge)") : ""}`);
10611
+ console.log(` ${chalk29.dim("Deduplication:")} ${dedupEnabled ? chalk29.green("enabled") : chalk29.yellow("disabled")}${autoMerge ? chalk29.dim(" (auto-merge)") : ""}`);
10289
10612
  }
10290
10613
  }
10291
10614
  } else {
10292
- console.log(` ${chalk28.dim("Not found")}`);
10615
+ console.log(` ${chalk29.dim("Not found")}`);
10293
10616
  }
10294
10617
  console.log("");
10295
- console.log(chalk28.underline("Authentication"));
10618
+ console.log(chalk29.underline("Authentication"));
10296
10619
  if (projectConfigInfo.token) {
10297
10620
  const maskedToken = projectConfigInfo.token.length > 8 ? `${projectConfigInfo.token.slice(0, 4)}...${projectConfigInfo.token.slice(-4)}` : "****";
10298
- const sourceLabel = projectConfigInfo.tokenSource === "env" ? chalk28.cyan("(from TEST_BRO_TOKEN env)") : chalk28.dim("(from user config)");
10299
- console.log(` ${chalk28.dim("Token:")} ${maskedToken} ${sourceLabel}`);
10621
+ const sourceLabel = projectConfigInfo.tokenSource === "env" ? chalk29.cyan("(from TEST_BRO_TOKEN env)") : chalk29.dim("(from user config)");
10622
+ console.log(` ${chalk29.dim("Token:")} ${maskedToken} ${sourceLabel}`);
10300
10623
  } else {
10301
- console.log(` ${chalk28.yellow("No token configured")}`);
10302
- console.log(chalk28.dim(" Set via: testbro init (or testbro login --token <token>)"));
10303
- console.log(chalk28.dim(" Or set TEST_BRO_TOKEN environment variable"));
10624
+ console.log(` ${chalk29.yellow("No token configured")}`);
10625
+ console.log(chalk29.dim(" Set via: testbro init (or testbro login --token <token>)"));
10626
+ console.log(chalk29.dim(" Or set TEST_BRO_TOKEN environment variable"));
10304
10627
  }
10305
10628
  if (projectConfigInfo.openRouterApiKey) {
10306
10629
  const maskedKey = projectConfigInfo.openRouterApiKey.length > 8 ? `${projectConfigInfo.openRouterApiKey.slice(0, 6)}...${projectConfigInfo.openRouterApiKey.slice(-4)}` : "****";
10307
- console.log(` ${chalk28.dim("OpenRouter API Key:")} ${maskedKey} ${chalk28.cyan("(from OPENROUTER_API_KEY env)")}`);
10630
+ console.log(` ${chalk29.dim("OpenRouter API Key:")} ${maskedKey} ${chalk29.cyan("(from OPENROUTER_API_KEY env)")}`);
10308
10631
  } else {
10309
- console.log(` ${chalk28.dim("OpenRouter API Key:")} ${chalk28.yellow("Not set")} ${chalk28.dim("(required for AI mode)")}`);
10632
+ console.log(` ${chalk29.dim("OpenRouter API Key:")} ${chalk29.yellow("Not set")} ${chalk29.dim("(required for AI mode)")}`);
10310
10633
  }
10311
10634
  console.log("");
10312
10635
  }
@@ -10320,11 +10643,11 @@ configCmd.command("init").description("Create a test-bro.config.json template in
10320
10643
  try {
10321
10644
  const { createConfigFile: createConfigFile2 } = (init_project_config(), __toCommonJS(project_config_exports));
10322
10645
  const configPath = createConfigFile2();
10323
- console.log(chalk28.green(`Created ${configPath}`));
10646
+ console.log(chalk29.green(`Created ${configPath}`));
10324
10647
  console.log("");
10325
- console.log(chalk28.dim("Edit this file to configure your project settings."));
10326
- console.log(chalk28.dim("Note: Never store tokens in this file. Use .env or testbro login."));
10327
- console.log(chalk28.dim("Tip: Use 'testbro init' for interactive setup with authentication."));
10648
+ console.log(chalk29.dim("Edit this file to configure your project settings."));
10649
+ console.log(chalk29.dim("Note: Never store tokens in this file. Use .env or testbro login."));
10650
+ console.log(chalk29.dim("Tip: Use 'testbro init' for interactive setup with authentication."));
10328
10651
  } catch (error) {
10329
10652
  if (error instanceof Error) {
10330
10653
  displayError(error.message);
@@ -10338,7 +10661,11 @@ configCmd.option("--api-url <url>", "Set the API URL in user config").hook("preA
10338
10661
  if (options.apiUrl) {
10339
10662
  const { setApiUrl: setApiUrl2 } = (init_config(), __toCommonJS(config_exports));
10340
10663
  setApiUrl2(options.apiUrl);
10341
- console.log(chalk28.green(`API URL updated to: ${options.apiUrl}`));
10664
+ console.log(chalk29.green(`API URL updated to: ${options.apiUrl}`));
10342
10665
  }
10343
10666
  });
10667
+ var showVersionNotice = startVersionCheck(CLI_VERSION);
10668
+ program.hook("postAction", async () => {
10669
+ await showVersionNotice();
10670
+ });
10344
10671
  program.parse();
package/package.json CHANGED
@@ -3,11 +3,11 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0",
6
+ "version": "0.1.2",
7
7
  "description": "TestBro CLI - AI-powered browser testing from your terminal",
8
8
  "type": "module",
9
9
  "bin": {
10
- "testbro": "./dist/index.js"
10
+ "testbro": "dist/index.js"
11
11
  },
12
12
  "files": [
13
13
  "dist"
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/chernobelenkiy/test-bro.git",
29
+ "url": "git+https://github.com/chernobelenkiy/test-bro.git",
30
30
  "directory": "apps/cli"
31
31
  },
32
32
  "license": "MIT",