@hasna/testers 0.0.45 → 0.0.46

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.
package/dist/cli/index.js CHANGED
@@ -2142,6 +2142,12 @@ function workflowExecutionFromValue(value) {
2142
2142
  const sandboxSyncStrategy = syncStrategyValue(input["sandboxSyncStrategy"]);
2143
2143
  const setupCommand = stringValue(input["setupCommand"]);
2144
2144
  const packageSpec = stringValue(input["packageSpec"]);
2145
+ const appSourceDir = stringValue(input["appSourceDir"]);
2146
+ const appRemoteDir = stringValue(input["appRemoteDir"]);
2147
+ const appStartCommand = stringValue(input["appStartCommand"]);
2148
+ const appUrl = stringValue(input["appUrl"]);
2149
+ const appWaitUrl = stringValue(input["appWaitUrl"]);
2150
+ const appWaitTimeoutMs = numberValue(input["appWaitTimeoutMs"]);
2145
2151
  const timeoutMs = numberValue(input["timeoutMs"]);
2146
2152
  const env = stringMap(input["env"]);
2147
2153
  return {
@@ -2153,6 +2159,12 @@ function workflowExecutionFromValue(value) {
2153
2159
  ...sandboxSyncStrategy ? { sandboxSyncStrategy } : {},
2154
2160
  ...setupCommand ? { setupCommand } : {},
2155
2161
  ...packageSpec ? { packageSpec } : {},
2162
+ ...appSourceDir ? { appSourceDir } : {},
2163
+ ...appRemoteDir ? { appRemoteDir } : {},
2164
+ ...appStartCommand ? { appStartCommand } : {},
2165
+ ...appUrl ? { appUrl } : {},
2166
+ ...appWaitUrl ? { appWaitUrl } : {},
2167
+ ...appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs } : {},
2156
2168
  ...timeoutMs !== undefined ? { timeoutMs } : {},
2157
2169
  ...env ? { env } : {}
2158
2170
  };
@@ -27027,9 +27039,10 @@ var init_workflows = __esm(() => {
27027
27039
  });
27028
27040
 
27029
27041
  // src/lib/workflow-runner.ts
27030
- import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
27042
+ import { spawnSync } from "child_process";
27043
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, mkdtempSync, rmSync, statSync } from "fs";
27031
27044
  import { tmpdir } from "os";
27032
- import { join as join16 } from "path";
27045
+ import { join as join16, posix as pathPosix } from "path";
27033
27046
  function buildWorkflowRunPlan(workflow, options) {
27034
27047
  const runOptions = {
27035
27048
  url: options.url,
@@ -27071,10 +27084,16 @@ function createWorkflowDatabaseBundle(workflow, plan) {
27071
27084
  if (!plan.sandbox)
27072
27085
  throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
27073
27086
  const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
27074
- writeFileSync4(join16(localDir, "testers.db"), getDatabase().serialize());
27087
+ const stateDir = join16(localDir, ".testers-state");
27088
+ mkdirSync11(stateDir, { recursive: true });
27089
+ writeDatabaseSnapshot(join16(stateDir, "testers.db"));
27090
+ if (plan.sandbox.appSourceDir && plan.sandbox.appRemoteDir) {
27091
+ const relativeAppDir = relativeRemotePath(plan.sandbox.remoteDir, plan.sandbox.appRemoteDir);
27092
+ copyAppSource(plan.sandbox.appSourceDir, join16(localDir, relativeAppDir));
27093
+ }
27075
27094
  return {
27076
27095
  localDir,
27077
- remoteDir: plan.sandbox.stateRemoteDir,
27096
+ remoteDir: plan.sandbox.remoteDir,
27078
27097
  cleanup: () => rmSync(localDir, { recursive: true, force: true })
27079
27098
  };
27080
27099
  }
@@ -27085,15 +27104,63 @@ function validatePersonaIds(workflow) {
27085
27104
  }
27086
27105
  }
27087
27106
  }
27107
+ function relativeRemotePath(remoteDir, remoteChildDir) {
27108
+ if (!remoteChildDir.startsWith("/")) {
27109
+ const relative3 = remoteChildDir.replace(/^\/+/, "").replace(/\/+$/, "");
27110
+ if (!relative3 || relative3 === ".") {
27111
+ throw new Error("Sandbox app remote directory must be a child directory, not the workflow root");
27112
+ }
27113
+ return relative3;
27114
+ }
27115
+ const base = remoteDir.replace(/\/+$/, "") || "/";
27116
+ const child = remoteChildDir.replace(/\/+$/, "") || "/";
27117
+ const relative2 = pathPosix.relative(base, child);
27118
+ if (!relative2 || relative2 === "." || relative2.startsWith("..") || pathPosix.isAbsolute(relative2)) {
27119
+ throw new Error(`Sandbox app remote directory must be inside the workflow remote directory (${remoteDir}): ${remoteChildDir}`);
27120
+ }
27121
+ return relative2;
27122
+ }
27123
+ function copyAppSource(sourceDir, targetDir) {
27124
+ if (!existsSync14(sourceDir) || !statSync(sourceDir).isDirectory()) {
27125
+ throw new Error(`Sandbox app source directory does not exist or is not a directory: ${sourceDir}`);
27126
+ }
27127
+ mkdirSync11(targetDir, { recursive: true });
27128
+ const result = spawnSync("rsync", [
27129
+ "-a",
27130
+ "--delete",
27131
+ ...APP_SOURCE_EXCLUDES.flatMap((item) => ["--exclude", item]),
27132
+ `${sourceDir.replace(/\/+$/, "")}/`,
27133
+ `${targetDir.replace(/\/+$/, "")}/`
27134
+ ], { encoding: "utf8" });
27135
+ if (result.error) {
27136
+ throw new Error(`Failed to rsync sandbox app source: ${result.error.message}`);
27137
+ }
27138
+ if (result.status !== 0) {
27139
+ throw new Error(`Failed to rsync sandbox app source (${result.status}): ${result.stderr.trim()}`);
27140
+ }
27141
+ }
27142
+ function writeDatabaseSnapshot(targetPath) {
27143
+ getDatabase().exec(`VACUUM INTO ${sqlString(targetPath)}`);
27144
+ }
27145
+ function sqlString(value) {
27146
+ return `'${value.replaceAll("'", "''")}'`;
27147
+ }
27088
27148
  function buildSandboxPlan(workflow, execution, runOptions) {
27089
27149
  const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
27090
27150
  const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
27151
+ const appRemoteDir = execution.appSourceDir ? execution.appRemoteDir ?? `${remoteDir.replace(/\/+$/, "")}/app` : execution.appRemoteDir;
27091
27152
  return {
27092
27153
  provider: execution.provider,
27093
27154
  image: execution.sandboxImage,
27094
27155
  name: `testers-${workflow.id.slice(0, 8)}`,
27095
27156
  remoteDir,
27096
27157
  stateRemoteDir,
27158
+ ...execution.appSourceDir ? { appSourceDir: execution.appSourceDir } : {},
27159
+ ...appRemoteDir ? { appRemoteDir } : {},
27160
+ ...execution.appStartCommand ? { appStartCommand: execution.appStartCommand } : {},
27161
+ ...execution.appUrl ? { appUrl: execution.appUrl } : {},
27162
+ ...execution.appWaitUrl ? { appWaitUrl: execution.appWaitUrl } : {},
27163
+ ...execution.appWaitTimeoutMs !== undefined ? { appWaitTimeoutMs: execution.appWaitTimeoutMs } : {},
27097
27164
  cleanup: execution.sandboxCleanup ?? "delete",
27098
27165
  syncStrategy: execution.sandboxSyncStrategy ?? "rsync",
27099
27166
  timeoutMs: execution.timeoutMs,
@@ -27101,6 +27168,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
27101
27168
  command: buildSandboxCommand({
27102
27169
  runOptions,
27103
27170
  remoteDir,
27171
+ stateRemoteDir,
27172
+ appRemoteDir,
27173
+ appStartCommand: execution.appStartCommand,
27174
+ appUrl: execution.appUrl,
27175
+ appWaitUrl: execution.appWaitUrl,
27176
+ appWaitTimeoutMs: execution.appWaitTimeoutMs,
27104
27177
  dbPath: `${stateRemoteDir}/testers.db`,
27105
27178
  setupCommand: execution.setupCommand,
27106
27179
  packageSpec: execution.packageSpec ?? "@hasna/testers"
@@ -27108,11 +27181,12 @@ function buildSandboxPlan(workflow, execution, runOptions) {
27108
27181
  };
27109
27182
  }
27110
27183
  function buildSandboxCommand(input) {
27184
+ const targetUrl = input.appUrl ?? input.runOptions.url;
27111
27185
  const args = [
27112
27186
  "bunx",
27113
27187
  input.packageSpec,
27114
27188
  "run",
27115
- input.runOptions.url,
27189
+ targetUrl,
27116
27190
  ...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
27117
27191
  ...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
27118
27192
  ...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
@@ -27128,12 +27202,46 @@ function buildSandboxCommand(input) {
27128
27202
  return [
27129
27203
  "set -euo pipefail",
27130
27204
  `mkdir -p ${shellQuote(input.remoteDir)}`,
27131
- `cd ${shellQuote(input.remoteDir)}`,
27205
+ `mkdir -p ${shellQuote(input.stateRemoteDir)}`,
27206
+ input.appRemoteDir ? `mkdir -p ${shellQuote(input.appRemoteDir)}` : undefined,
27207
+ `cd ${shellQuote(input.appRemoteDir ?? input.remoteDir)}`,
27132
27208
  input.setupCommand,
27209
+ buildAppStartCommand(input),
27133
27210
  `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
27134
27211
  ].filter(Boolean).join(`
27135
27212
  `);
27136
27213
  }
27214
+ function buildAppStartCommand(input) {
27215
+ if (!input.appStartCommand)
27216
+ return;
27217
+ const waitUrl = input.appWaitUrl ?? input.appUrl ?? input.runOptions.url;
27218
+ const waitTimeoutMs = input.appWaitTimeoutMs ?? 120000;
27219
+ return [
27220
+ `( ${input.appStartCommand} ) > ${shellQuote(`${input.stateRemoteDir}/app.log`)} 2>&1 &`,
27221
+ "APP_PID=$!",
27222
+ `echo "$APP_PID" > ${shellQuote(`${input.stateRemoteDir}/app.pid`)}`,
27223
+ `trap 'kill "$APP_PID" 2>/dev/null || true' EXIT`,
27224
+ waitUrl ? buildWaitForUrlCommand(waitUrl, waitTimeoutMs) : undefined
27225
+ ].filter(Boolean).join(`
27226
+ `);
27227
+ }
27228
+ function buildWaitForUrlCommand(url, timeoutMs) {
27229
+ const script = `
27230
+ const url = ${JSON.stringify(url)};
27231
+ const timeoutMs = ${JSON.stringify(timeoutMs)};
27232
+ const deadline = Date.now() + timeoutMs;
27233
+ while (Date.now() <= deadline) {
27234
+ try {
27235
+ const response = await fetch(url);
27236
+ if (response.status >= 200 && response.status < 500) process.exit(0);
27237
+ } catch {}
27238
+ await new Promise((resolve) => setTimeout(resolve, 1000));
27239
+ }
27240
+ console.error(\`Timed out waiting for \${url} after \${timeoutMs}ms\`);
27241
+ process.exit(1);
27242
+ `.trim();
27243
+ return `bun -e ${shellQuote(script)}`;
27244
+ }
27137
27245
  async function runViaSandbox(plan, dependencies) {
27138
27246
  if (!plan.sandbox)
27139
27247
  throw new Error("Workflow does not have a sandbox plan");
@@ -27191,11 +27299,22 @@ async function resolveSandboxesRuntime(dependencies) {
27191
27299
  function shellQuote(value) {
27192
27300
  return `'${value.replaceAll("'", `'"'"'`)}'`;
27193
27301
  }
27302
+ var APP_SOURCE_EXCLUDES;
27194
27303
  var init_workflow_runner = __esm(() => {
27195
27304
  init_database();
27196
27305
  init_workflows();
27197
27306
  init_personas();
27198
27307
  init_runner();
27308
+ APP_SOURCE_EXCLUDES = [
27309
+ "node_modules",
27310
+ ".git",
27311
+ "dist",
27312
+ ".next",
27313
+ ".turbo",
27314
+ ".cache",
27315
+ ".venv",
27316
+ "__pycache__"
27317
+ ];
27199
27318
  });
27200
27319
 
27201
27320
  // src/lib/ci.ts
@@ -27760,9 +27879,9 @@ import { webcrypto as crypto2 } from "crypto";
27760
27879
  import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
27761
27880
  import { join as join42 } from "path";
27762
27881
  import { Database as Database4 } from "bun:sqlite";
27763
- import { existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
27882
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
27764
27883
  import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
27765
- import { existsSync as existsSync22, writeFileSync as writeFileSync7 } from "fs";
27884
+ import { existsSync as existsSync22, writeFileSync as writeFileSync6 } from "fs";
27766
27885
  import { join as join22 } from "path";
27767
27886
  import { execSync as execSync3, execFileSync } from "child_process";
27768
27887
  import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
@@ -27774,7 +27893,7 @@ import * as zlib from "zlib";
27774
27893
  import { Readable } from "stream";
27775
27894
  import { Writable } from "stream";
27776
27895
  import { createHash as createHash22 } from "crypto";
27777
- import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync42 } from "fs";
27896
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync42 } from "fs";
27778
27897
  import { dirname as dirname42, join as join62, relative as relative3 } from "path";
27779
27898
  import { readdir, readFile as readFile2 } from "fs/promises";
27780
27899
  import { existsSync as existsSync62, readdirSync as readdirSync5, readFileSync as readFileSync42, statSync as statSync22 } from "fs";
@@ -27942,8 +28061,8 @@ function ensureDir2(filePath) {
27942
28061
  if (filePath === ":memory:")
27943
28062
  return;
27944
28063
  const dir = dirname4(resolve2(filePath));
27945
- if (!existsSync16(dir)) {
27946
- mkdirSync13(dir, { recursive: true });
28064
+ if (!existsSync17(dir)) {
28065
+ mkdirSync14(dir, { recursive: true });
27947
28066
  }
27948
28067
  }
27949
28068
  function getDatabase2(path) {
@@ -27981,7 +28100,7 @@ function gitInit(project) {
27981
28100
  execSync3("git init", { cwd: path, stdio: "pipe" });
27982
28101
  const gitignorePath = join22(path, ".gitignore");
27983
28102
  if (!existsSync22(gitignorePath)) {
27984
- writeFileSync7(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
28103
+ writeFileSync6(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
27985
28104
  }
27986
28105
  const projectJson = {
27987
28106
  id,
@@ -27990,7 +28109,7 @@ function gitInit(project) {
27990
28109
  created_at: project.created_at,
27991
28110
  integrations: project.integrations ?? {}
27992
28111
  };
27993
- writeFileSync7(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
28112
+ writeFileSync6(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
27994
28113
  `, "utf-8");
27995
28114
  execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
27996
28115
  execSync3(`git commit -m "chore: init project ${name}"`, {
@@ -29302,7 +29421,7 @@ async function collectLocalFiles(rootPath) {
29302
29421
  if (entry.isDirectory()) {
29303
29422
  await walk(fullPath);
29304
29423
  } else if (entry.isFile()) {
29305
- const stat = statSync2(fullPath);
29424
+ const stat = statSync3(fullPath);
29306
29425
  if (stat.size > MAX_FILE_SIZE)
29307
29426
  continue;
29308
29427
  const relPath = relative3(rootPath, fullPath);
@@ -94346,7 +94465,7 @@ import chalk6 from "chalk";
94346
94465
  // package.json
94347
94466
  var package_default = {
94348
94467
  name: "@hasna/testers",
94349
- version: "0.0.45",
94468
+ version: "0.0.46",
94350
94469
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
94351
94470
  type: "module",
94352
94471
  main: "dist/index.js",
@@ -94445,7 +94564,7 @@ init_todos_connector();
94445
94564
  init_browser();
94446
94565
  import { render, Box, Text, useInput, useApp } from "ink";
94447
94566
  import React, { useState } from "react";
94448
- import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8 } from "fs";
94567
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
94449
94568
  import { createInterface } from "readline";
94450
94569
  import { join as join20, resolve as resolve4 } from "path";
94451
94570
 
@@ -96533,18 +96652,18 @@ init_ci();
96533
96652
  init_assertions();
96534
96653
  init_paths();
96535
96654
  init_sessions();
96536
- import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
96655
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
96537
96656
 
96538
96657
  // src/lib/repo-discovery.ts
96539
96658
  init_paths();
96540
- import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync11, unlinkSync } from "fs";
96659
+ import { existsSync as existsSync15, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync12, unlinkSync } from "fs";
96541
96660
  import { createHash } from "crypto";
96542
96661
  import { join as join17, resolve, relative as relative2 } from "path";
96543
96662
  function getCacheDir() {
96544
96663
  const testersDir = getTestersDir();
96545
96664
  const cacheDir = join17(testersDir, "repo-index");
96546
- if (!existsSync14(cacheDir)) {
96547
- mkdirSync11(cacheDir, { recursive: true });
96665
+ if (!existsSync15(cacheDir)) {
96666
+ mkdirSync12(cacheDir, { recursive: true });
96548
96667
  }
96549
96668
  return cacheDir;
96550
96669
  }
@@ -96557,10 +96676,10 @@ function getCachePath(repoPath) {
96557
96676
  function isCacheStale(cached, repoPath) {
96558
96677
  for (const spec of cached.specs) {
96559
96678
  const fullPath = join17(repoPath, spec.file);
96560
- if (!existsSync14(fullPath))
96679
+ if (!existsSync15(fullPath))
96561
96680
  return true;
96562
96681
  try {
96563
- const stat = statSync(fullPath);
96682
+ const stat = statSync2(fullPath);
96564
96683
  if (stat.mtimeMs !== spec.mtimeMs)
96565
96684
  return true;
96566
96685
  } catch {
@@ -96569,10 +96688,10 @@ function isCacheStale(cached, repoPath) {
96569
96688
  }
96570
96689
  if (cached.configPath) {
96571
96690
  const configFullPath = join17(repoPath, cached.configPath);
96572
- if (!existsSync14(configFullPath))
96691
+ if (!existsSync15(configFullPath))
96573
96692
  return true;
96574
96693
  try {
96575
- statSync(configFullPath);
96694
+ statSync2(configFullPath);
96576
96695
  const age = Date.now() - new Date(cached.snapshotAt).getTime();
96577
96696
  if (age > 3600000)
96578
96697
  return true;
@@ -96584,7 +96703,7 @@ function isCacheStale(cached, repoPath) {
96584
96703
  }
96585
96704
  function loadCache(repoPath) {
96586
96705
  const cachePath = getCachePath(repoPath);
96587
- if (!existsSync14(cachePath))
96706
+ if (!existsSync15(cachePath))
96588
96707
  return null;
96589
96708
  try {
96590
96709
  const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
@@ -96595,14 +96714,14 @@ function loadCache(repoPath) {
96595
96714
  }
96596
96715
  function saveCache(snapshot) {
96597
96716
  const cachePath = getCachePath(snapshot.repoPath);
96598
- writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
96717
+ writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
96599
96718
  }
96600
96719
  function detectPackageManager(repoPath) {
96601
96720
  const result = {
96602
- npm: existsSync14(join17(repoPath, "package-lock.json")),
96603
- yarn: existsSync14(join17(repoPath, "yarn.lock")),
96604
- pnpm: existsSync14(join17(repoPath, "pnpm-lock.yaml")),
96605
- bun: existsSync14(join17(repoPath, "bun.lockb")) || existsSync14(join17(repoPath, "bun.lock")),
96721
+ npm: existsSync15(join17(repoPath, "package-lock.json")),
96722
+ yarn: existsSync15(join17(repoPath, "yarn.lock")),
96723
+ pnpm: existsSync15(join17(repoPath, "pnpm-lock.yaml")),
96724
+ bun: existsSync15(join17(repoPath, "bun.lockb")) || existsSync15(join17(repoPath, "bun.lock")),
96606
96725
  preferred: "npm"
96607
96726
  };
96608
96727
  if (result.bun)
@@ -96617,7 +96736,7 @@ function detectPackageManager(repoPath) {
96617
96736
  }
96618
96737
  function detectDevScripts(repoPath) {
96619
96738
  const pkgPath = join17(repoPath, "package.json");
96620
- if (!existsSync14(pkgPath)) {
96739
+ if (!existsSync15(pkgPath)) {
96621
96740
  return { dev: null, test: null, seed: null, build: null };
96622
96741
  }
96623
96742
  let scripts;
@@ -96643,7 +96762,7 @@ function findPlaywrightConfig(repoPath) {
96643
96762
  "playwright-ct.config.js"
96644
96763
  ];
96645
96764
  for (const name of candidates) {
96646
- if (existsSync14(join17(repoPath, name)))
96765
+ if (existsSync15(join17(repoPath, name)))
96647
96766
  return name;
96648
96767
  }
96649
96768
  return null;
@@ -96702,7 +96821,7 @@ function findSpecFiles(repoPath, globPatterns) {
96702
96821
  const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
96703
96822
  for (const dir of dirsToSearch) {
96704
96823
  const searchDir = dir ? join17(repoPath, dir) : repoPath;
96705
- if (!existsSync14(searchDir))
96824
+ if (!existsSync15(searchDir))
96706
96825
  continue;
96707
96826
  try {
96708
96827
  const files = walkDir(searchDir);
@@ -96714,7 +96833,7 @@ function findSpecFiles(repoPath, globPatterns) {
96714
96833
  seen.add(relativePath);
96715
96834
  const content = readFileSync6(file, "utf-8");
96716
96835
  const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
96717
- const stat = statSync(file);
96836
+ const stat = statSync2(file);
96718
96837
  specs.push({
96719
96838
  file: relativePath,
96720
96839
  fromGlob: pattern,
@@ -96754,7 +96873,7 @@ function matchesGlob(filePath, pattern) {
96754
96873
  }
96755
96874
  function detectSuggestedUrl(repoPath) {
96756
96875
  const pkgPath = join17(repoPath, "package.json");
96757
- if (!existsSync14(pkgPath))
96876
+ if (!existsSync15(pkgPath))
96758
96877
  return null;
96759
96878
  try {
96760
96879
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
@@ -96774,10 +96893,10 @@ function detectSuggestedUrl(repoPath) {
96774
96893
  }
96775
96894
  function checkPlaywrightBrowserInstalled(repoPath) {
96776
96895
  const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
96777
- if (existsSync14(cacheDir))
96896
+ if (existsSync15(cacheDir))
96778
96897
  return true;
96779
96898
  const globalCache = join17(repoPath, ".cache", "ms-playwright");
96780
- if (existsSync14(globalCache))
96899
+ if (existsSync15(globalCache))
96781
96900
  return true;
96782
96901
  return false;
96783
96902
  }
@@ -96817,7 +96936,7 @@ function discoverRepo(opts) {
96817
96936
  const specs = findSpecFiles(repoPath, globPatterns);
96818
96937
  const packageManager = detectPackageManager(repoPath);
96819
96938
  const devScripts = detectDevScripts(repoPath);
96820
- const playwrightInstalled = existsSync14(join17(repoPath, "node_modules", "playwright")) || existsSync14(join17(repoPath, "node_modules", "@playwright", "test"));
96939
+ const playwrightInstalled = existsSync15(join17(repoPath, "node_modules", "playwright")) || existsSync15(join17(repoPath, "node_modules", "@playwright", "test"));
96821
96940
  const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
96822
96941
  const configExists = configPath !== null;
96823
96942
  const specsFound = specs.length > 0;
@@ -96876,11 +96995,11 @@ function discoverRepo(opts) {
96876
96995
  }
96877
96996
  function clearDiscoveryCache(repoPath) {
96878
96997
  const cacheDir = getCacheDir();
96879
- if (!existsSync14(cacheDir))
96998
+ if (!existsSync15(cacheDir))
96880
96999
  return;
96881
97000
  if (repoPath) {
96882
97001
  const cachePath = getCachePath(repoPath);
96883
- if (existsSync14(cachePath)) {
97002
+ if (existsSync15(cachePath)) {
96884
97003
  unlinkSync(cachePath);
96885
97004
  }
96886
97005
  } else {
@@ -96893,7 +97012,7 @@ function clearDiscoveryCache(repoPath) {
96893
97012
  }
96894
97013
  function getDiscoveryCacheInfo(repoPath) {
96895
97014
  const cachePath = getCachePath(repoPath);
96896
- if (!existsSync14(cachePath))
97015
+ if (!existsSync15(cachePath))
96897
97016
  return null;
96898
97017
  const cached = loadCache(repoPath);
96899
97018
  if (!cached)
@@ -96910,11 +97029,11 @@ init_runs();
96910
97029
  init_database();
96911
97030
  init_paths();
96912
97031
  import { execSync as execSync2 } from "child_process";
96913
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "fs";
97032
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync5 } from "fs";
96914
97033
  import { join as join18 } from "path";
96915
97034
  function resolvePlaywrightCmd(repoPath) {
96916
97035
  const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
96917
- if (existsSync15(localPw)) {
97036
+ if (existsSync16(localPw)) {
96918
97037
  return [localPw, "test"];
96919
97038
  }
96920
97039
  return ["npx", "playwright", "test"];
@@ -97110,9 +97229,9 @@ async function runRepoTests(opts) {
97110
97229
  const resultRecord = { id: resultId };
97111
97230
  if (result.stdout || result.stderr) {
97112
97231
  const reportersDir = join18(getTestersDir(), "repo-run-output");
97113
- mkdirSync12(reportersDir, { recursive: true });
97232
+ mkdirSync13(reportersDir, { recursive: true });
97114
97233
  const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
97115
- writeFileSync6(outputFile, `=== stdout ===
97234
+ writeFileSync5(outputFile, `=== stdout ===
97116
97235
  ${result.stdout}
97117
97236
 
97118
97237
  === stderr ===
@@ -97519,7 +97638,7 @@ program2.command("prod-debug <target>").description("Create a safe production de
97519
97638
  }, config2.prodDebug);
97520
97639
  const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
97521
97640
  if (opts.output) {
97522
- writeFileSync8(resolve4(opts.output), output + `
97641
+ writeFileSync7(resolve4(opts.output), output + `
97523
97642
  `);
97524
97643
  } else {
97525
97644
  log(output);
@@ -97529,7 +97648,7 @@ var CONFIG_DIR5 = getTestersDir();
97529
97648
  var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
97530
97649
  function getActiveProject() {
97531
97650
  try {
97532
- if (existsSync17(CONFIG_PATH4)) {
97651
+ if (existsSync18(CONFIG_PATH4)) {
97533
97652
  const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
97534
97653
  return raw.activeProject ?? undefined;
97535
97654
  }
@@ -98083,7 +98202,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98083
98202
  if (opts.json || opts.output) {
98084
98203
  const jsonOutput = formatJSON(run3, results2);
98085
98204
  if (opts.output) {
98086
- writeFileSync8(opts.output, jsonOutput, "utf-8");
98205
+ writeFileSync7(opts.output, jsonOutput, "utf-8");
98087
98206
  log(chalk6.green(`Results written to ${opts.output}`));
98088
98207
  }
98089
98208
  if (opts.json) {
@@ -98192,7 +98311,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98192
98311
  if (opts.json || opts.output) {
98193
98312
  const jsonOutput = formatJSON(run2, results);
98194
98313
  if (opts.output) {
98195
- writeFileSync8(opts.output, jsonOutput, "utf-8");
98314
+ writeFileSync7(opts.output, jsonOutput, "utf-8");
98196
98315
  log(chalk6.green(`Results written to ${opts.output}`));
98197
98316
  }
98198
98317
  if (opts.json) {
@@ -98445,13 +98564,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98445
98564
  if (fmt === "json") {
98446
98565
  const outputPath = opts.output ?? "testers-export.json";
98447
98566
  const data = JSON.stringify(scenarios, null, 2);
98448
- writeFileSync8(outputPath, data, "utf-8");
98567
+ writeFileSync7(outputPath, data, "utf-8");
98449
98568
  log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
98450
98569
  return;
98451
98570
  }
98452
98571
  const outputDir = opts.output ?? ".";
98453
- if (!existsSync17(outputDir)) {
98454
- mkdirSync14(outputDir, { recursive: true });
98572
+ if (!existsSync18(outputDir)) {
98573
+ mkdirSync15(outputDir, { recursive: true });
98455
98574
  }
98456
98575
  for (const s2 of scenarios) {
98457
98576
  const lines = [];
@@ -98479,7 +98598,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98479
98598
  }
98480
98599
  const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
98481
98600
  const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
98482
- writeFileSync8(filePath, lines.join(`
98601
+ writeFileSync7(filePath, lines.join(`
98483
98602
  `), "utf-8");
98484
98603
  log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
98485
98604
  }
@@ -98653,17 +98772,17 @@ projectCmd.command("export-open <id>").description("Register a testers project i
98653
98772
  projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
98654
98773
  try {
98655
98774
  const project = ensureProject(name21, process.cwd());
98656
- if (!existsSync17(CONFIG_DIR5)) {
98657
- mkdirSync14(CONFIG_DIR5, { recursive: true });
98775
+ if (!existsSync18(CONFIG_DIR5)) {
98776
+ mkdirSync15(CONFIG_DIR5, { recursive: true });
98658
98777
  }
98659
98778
  let config2 = {};
98660
- if (existsSync17(CONFIG_PATH4)) {
98779
+ if (existsSync18(CONFIG_PATH4)) {
98661
98780
  try {
98662
98781
  config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
98663
98782
  } catch {}
98664
98783
  }
98665
98784
  config2.activeProject = project.id;
98666
- writeFileSync8(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
98785
+ writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
98667
98786
  if (opts.json) {
98668
98787
  log(JSON.stringify({ activeProject: project.id, project }, null, 2));
98669
98788
  return;
@@ -99272,10 +99391,10 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
99272
99391
  if (opts.output) {
99273
99392
  const outPath = resolve4(opts.output);
99274
99393
  const outDir = outPath.replace(/\/[^/]*$/, "");
99275
- if (outDir && !existsSync17(outDir)) {
99276
- mkdirSync14(outDir, { recursive: true });
99394
+ if (outDir && !existsSync18(outDir)) {
99395
+ mkdirSync15(outDir, { recursive: true });
99277
99396
  }
99278
- writeFileSync8(outPath, workflow, "utf-8");
99397
+ writeFileSync7(outPath, workflow, "utf-8");
99279
99398
  log(chalk6.green(`Workflow written to ${outPath}`));
99280
99399
  return;
99281
99400
  }
@@ -99307,11 +99426,11 @@ program2.command("init").description("Initialize a new testing project").option(
99307
99426
  }
99308
99427
  if (opts.ci === "github") {
99309
99428
  const workflowDir = join20(process.cwd(), ".github", "workflows");
99310
- if (!existsSync17(workflowDir)) {
99311
- mkdirSync14(workflowDir, { recursive: true });
99429
+ if (!existsSync18(workflowDir)) {
99430
+ mkdirSync15(workflowDir, { recursive: true });
99312
99431
  }
99313
99432
  const workflowPath = join20(workflowDir, "testers.yml");
99314
- writeFileSync8(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
99433
+ writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
99315
99434
  log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
99316
99435
  } else if (opts.ci) {
99317
99436
  log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
@@ -99504,7 +99623,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
99504
99623
  wcagLevel
99505
99624
  });
99506
99625
  if (opts.output) {
99507
- writeFileSync8(resolve4(opts.output), JSON.stringify(result, null, 2));
99626
+ writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
99508
99627
  }
99509
99628
  if (opts.json) {
99510
99629
  log(JSON.stringify(result, null, 2));
@@ -99555,7 +99674,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99555
99674
  format
99556
99675
  });
99557
99676
  if (opts.output && opts.output !== "report.html") {
99558
- writeFileSync8(opts.output, content, "utf-8");
99677
+ writeFileSync7(opts.output, content, "utf-8");
99559
99678
  const absPath2 = resolve4(opts.output);
99560
99679
  log(chalk6.green(`Compliance report written to ${absPath2}`));
99561
99680
  } else {
@@ -99569,7 +99688,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99569
99688
  } else {
99570
99689
  html = generateHtmlReport(runId);
99571
99690
  }
99572
- writeFileSync8(opts.output, html, "utf-8");
99691
+ writeFileSync7(opts.output, html, "utf-8");
99573
99692
  const absPath = resolve4(opts.output);
99574
99693
  log(chalk6.green(`Report generated: ${absPath}`));
99575
99694
  if (opts.open) {
@@ -100849,7 +100968,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100849
100968
  }, []).option("--priority <level>", "Scenario priority").option("--persona <ids>", "Comma-separated persona IDs").option("--goal <prompt>", "Goal prompt for the agentic testing loop").option("--success <criteria>", "Success criteria (repeatable)", (val, acc) => {
100850
100969
  acc.push(val);
100851
100970
  return acc;
100852
- }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
100971
+ }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-sync <strategy>", "Sandbox upload sync strategy: rsync or archive", "rsync").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--sandbox-app-source <path>", "Local app source directory to upload into the sandbox").option("--sandbox-app-remote-dir <path>", "Remote app directory inside the sandbox (default: <sandbox-remote-dir>/app)").option("--sandbox-app-start-command <command>", "Shell command to start the app before testers runs").option("--sandbox-app-url <url>", "URL testers should target inside the sandbox after the app starts").option("--sandbox-app-wait-url <url>", "URL to poll before starting testers (defaults to --sandbox-app-url)").option("--sandbox-app-wait-timeout <ms>", "App readiness wait timeout in milliseconds").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
100853
100972
  try {
100854
100973
  const workflow = createTestingWorkflow({
100855
100974
  name: name21,
@@ -100875,6 +100994,12 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100875
100994
  sandboxSyncStrategy: opts.sandboxSync,
100876
100995
  setupCommand: opts.sandboxSetupCommand,
100877
100996
  packageSpec: opts.sandboxPackage,
100997
+ appSourceDir: opts.sandboxAppSource,
100998
+ appRemoteDir: opts.sandboxAppRemoteDir,
100999
+ appStartCommand: opts.sandboxAppStartCommand,
101000
+ appUrl: opts.sandboxAppUrl,
101001
+ appWaitUrl: opts.sandboxAppWaitUrl,
101002
+ appWaitTimeoutMs: opts.sandboxAppWaitTimeout ? parseInt(opts.sandboxAppWaitTimeout, 10) : undefined,
100878
101003
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
100879
101004
  }
100880
101005
  });