@hasna/testers 0.0.45 → 0.0.47

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");
@@ -27154,7 +27262,7 @@ async function runViaSandbox(plan, dependencies) {
27154
27262
  workflowId: plan.workflow.id,
27155
27263
  workflowName: plan.workflow.name
27156
27264
  },
27157
- sandboxEnvVars: plan.sandbox.env,
27265
+ sandboxEnvVars: resolveSandboxEnv(plan.sandbox.env),
27158
27266
  cleanup: plan.sandbox.cleanup,
27159
27267
  upload: {
27160
27268
  localDir: bundle.localDir,
@@ -27180,6 +27288,19 @@ async function runViaSandbox(plan, dependencies) {
27180
27288
  bundle.cleanup?.();
27181
27289
  }
27182
27290
  }
27291
+ function resolveSandboxEnv(env) {
27292
+ if (!env || Object.keys(env).length === 0)
27293
+ return;
27294
+ const resolved = {};
27295
+ for (const [key, value] of Object.entries(env)) {
27296
+ const resolvedValue = resolveCredential(value);
27297
+ if (resolvedValue === null) {
27298
+ throw new Error(`Missing sandbox env value for ${key}`);
27299
+ }
27300
+ resolved[key] = resolvedValue;
27301
+ }
27302
+ return resolved;
27303
+ }
27183
27304
  async function resolveSandboxesRuntime(dependencies) {
27184
27305
  if (dependencies.sandboxes)
27185
27306
  return dependencies.sandboxes;
@@ -27191,11 +27312,23 @@ async function resolveSandboxesRuntime(dependencies) {
27191
27312
  function shellQuote(value) {
27192
27313
  return `'${value.replaceAll("'", `'"'"'`)}'`;
27193
27314
  }
27315
+ var APP_SOURCE_EXCLUDES;
27194
27316
  var init_workflow_runner = __esm(() => {
27195
27317
  init_database();
27196
27318
  init_workflows();
27197
27319
  init_personas();
27198
27320
  init_runner();
27321
+ init_secrets_resolver();
27322
+ APP_SOURCE_EXCLUDES = [
27323
+ "node_modules",
27324
+ ".git",
27325
+ "dist",
27326
+ ".next",
27327
+ ".turbo",
27328
+ ".cache",
27329
+ ".venv",
27330
+ "__pycache__"
27331
+ ];
27199
27332
  });
27200
27333
 
27201
27334
  // src/lib/ci.ts
@@ -27760,9 +27893,9 @@ import { webcrypto as crypto2 } from "crypto";
27760
27893
  import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
27761
27894
  import { join as join42 } from "path";
27762
27895
  import { Database as Database4 } from "bun:sqlite";
27763
- import { existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
27896
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
27764
27897
  import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
27765
- import { existsSync as existsSync22, writeFileSync as writeFileSync7 } from "fs";
27898
+ import { existsSync as existsSync22, writeFileSync as writeFileSync6 } from "fs";
27766
27899
  import { join as join22 } from "path";
27767
27900
  import { execSync as execSync3, execFileSync } from "child_process";
27768
27901
  import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
@@ -27774,7 +27907,7 @@ import * as zlib from "zlib";
27774
27907
  import { Readable } from "stream";
27775
27908
  import { Writable } from "stream";
27776
27909
  import { createHash as createHash22 } from "crypto";
27777
- import { mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync42 } from "fs";
27910
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync42 } from "fs";
27778
27911
  import { dirname as dirname42, join as join62, relative as relative3 } from "path";
27779
27912
  import { readdir, readFile as readFile2 } from "fs/promises";
27780
27913
  import { existsSync as existsSync62, readdirSync as readdirSync5, readFileSync as readFileSync42, statSync as statSync22 } from "fs";
@@ -27942,8 +28075,8 @@ function ensureDir2(filePath) {
27942
28075
  if (filePath === ":memory:")
27943
28076
  return;
27944
28077
  const dir = dirname4(resolve2(filePath));
27945
- if (!existsSync16(dir)) {
27946
- mkdirSync13(dir, { recursive: true });
28078
+ if (!existsSync17(dir)) {
28079
+ mkdirSync14(dir, { recursive: true });
27947
28080
  }
27948
28081
  }
27949
28082
  function getDatabase2(path) {
@@ -27981,7 +28114,7 @@ function gitInit(project) {
27981
28114
  execSync3("git init", { cwd: path, stdio: "pipe" });
27982
28115
  const gitignorePath = join22(path, ".gitignore");
27983
28116
  if (!existsSync22(gitignorePath)) {
27984
- writeFileSync7(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
28117
+ writeFileSync6(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
27985
28118
  }
27986
28119
  const projectJson = {
27987
28120
  id,
@@ -27990,7 +28123,7 @@ function gitInit(project) {
27990
28123
  created_at: project.created_at,
27991
28124
  integrations: project.integrations ?? {}
27992
28125
  };
27993
- writeFileSync7(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
28126
+ writeFileSync6(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
27994
28127
  `, "utf-8");
27995
28128
  execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
27996
28129
  execSync3(`git commit -m "chore: init project ${name}"`, {
@@ -29302,7 +29435,7 @@ async function collectLocalFiles(rootPath) {
29302
29435
  if (entry.isDirectory()) {
29303
29436
  await walk(fullPath);
29304
29437
  } else if (entry.isFile()) {
29305
- const stat = statSync2(fullPath);
29438
+ const stat = statSync3(fullPath);
29306
29439
  if (stat.size > MAX_FILE_SIZE)
29307
29440
  continue;
29308
29441
  const relPath = relative3(rootPath, fullPath);
@@ -94346,7 +94479,7 @@ import chalk6 from "chalk";
94346
94479
  // package.json
94347
94480
  var package_default = {
94348
94481
  name: "@hasna/testers",
94349
- version: "0.0.45",
94482
+ version: "0.0.47",
94350
94483
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
94351
94484
  type: "module",
94352
94485
  main: "dist/index.js",
@@ -94445,7 +94578,7 @@ init_todos_connector();
94445
94578
  init_browser();
94446
94579
  import { render, Box, Text, useInput, useApp } from "ink";
94447
94580
  import React, { useState } from "react";
94448
- import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8 } from "fs";
94581
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
94449
94582
  import { createInterface } from "readline";
94450
94583
  import { join as join20, resolve as resolve4 } from "path";
94451
94584
 
@@ -96533,18 +96666,18 @@ init_ci();
96533
96666
  init_assertions();
96534
96667
  init_paths();
96535
96668
  init_sessions();
96536
- import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
96669
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15 } from "fs";
96537
96670
 
96538
96671
  // src/lib/repo-discovery.ts
96539
96672
  init_paths();
96540
- import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync11, unlinkSync } from "fs";
96673
+ import { existsSync as existsSync15, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync12, unlinkSync } from "fs";
96541
96674
  import { createHash } from "crypto";
96542
96675
  import { join as join17, resolve, relative as relative2 } from "path";
96543
96676
  function getCacheDir() {
96544
96677
  const testersDir = getTestersDir();
96545
96678
  const cacheDir = join17(testersDir, "repo-index");
96546
- if (!existsSync14(cacheDir)) {
96547
- mkdirSync11(cacheDir, { recursive: true });
96679
+ if (!existsSync15(cacheDir)) {
96680
+ mkdirSync12(cacheDir, { recursive: true });
96548
96681
  }
96549
96682
  return cacheDir;
96550
96683
  }
@@ -96557,10 +96690,10 @@ function getCachePath(repoPath) {
96557
96690
  function isCacheStale(cached, repoPath) {
96558
96691
  for (const spec of cached.specs) {
96559
96692
  const fullPath = join17(repoPath, spec.file);
96560
- if (!existsSync14(fullPath))
96693
+ if (!existsSync15(fullPath))
96561
96694
  return true;
96562
96695
  try {
96563
- const stat = statSync(fullPath);
96696
+ const stat = statSync2(fullPath);
96564
96697
  if (stat.mtimeMs !== spec.mtimeMs)
96565
96698
  return true;
96566
96699
  } catch {
@@ -96569,10 +96702,10 @@ function isCacheStale(cached, repoPath) {
96569
96702
  }
96570
96703
  if (cached.configPath) {
96571
96704
  const configFullPath = join17(repoPath, cached.configPath);
96572
- if (!existsSync14(configFullPath))
96705
+ if (!existsSync15(configFullPath))
96573
96706
  return true;
96574
96707
  try {
96575
- statSync(configFullPath);
96708
+ statSync2(configFullPath);
96576
96709
  const age = Date.now() - new Date(cached.snapshotAt).getTime();
96577
96710
  if (age > 3600000)
96578
96711
  return true;
@@ -96584,7 +96717,7 @@ function isCacheStale(cached, repoPath) {
96584
96717
  }
96585
96718
  function loadCache(repoPath) {
96586
96719
  const cachePath = getCachePath(repoPath);
96587
- if (!existsSync14(cachePath))
96720
+ if (!existsSync15(cachePath))
96588
96721
  return null;
96589
96722
  try {
96590
96723
  const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
@@ -96595,14 +96728,14 @@ function loadCache(repoPath) {
96595
96728
  }
96596
96729
  function saveCache(snapshot) {
96597
96730
  const cachePath = getCachePath(snapshot.repoPath);
96598
- writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
96731
+ writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
96599
96732
  }
96600
96733
  function detectPackageManager(repoPath) {
96601
96734
  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")),
96735
+ npm: existsSync15(join17(repoPath, "package-lock.json")),
96736
+ yarn: existsSync15(join17(repoPath, "yarn.lock")),
96737
+ pnpm: existsSync15(join17(repoPath, "pnpm-lock.yaml")),
96738
+ bun: existsSync15(join17(repoPath, "bun.lockb")) || existsSync15(join17(repoPath, "bun.lock")),
96606
96739
  preferred: "npm"
96607
96740
  };
96608
96741
  if (result.bun)
@@ -96617,7 +96750,7 @@ function detectPackageManager(repoPath) {
96617
96750
  }
96618
96751
  function detectDevScripts(repoPath) {
96619
96752
  const pkgPath = join17(repoPath, "package.json");
96620
- if (!existsSync14(pkgPath)) {
96753
+ if (!existsSync15(pkgPath)) {
96621
96754
  return { dev: null, test: null, seed: null, build: null };
96622
96755
  }
96623
96756
  let scripts;
@@ -96643,7 +96776,7 @@ function findPlaywrightConfig(repoPath) {
96643
96776
  "playwright-ct.config.js"
96644
96777
  ];
96645
96778
  for (const name of candidates) {
96646
- if (existsSync14(join17(repoPath, name)))
96779
+ if (existsSync15(join17(repoPath, name)))
96647
96780
  return name;
96648
96781
  }
96649
96782
  return null;
@@ -96702,7 +96835,7 @@ function findSpecFiles(repoPath, globPatterns) {
96702
96835
  const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
96703
96836
  for (const dir of dirsToSearch) {
96704
96837
  const searchDir = dir ? join17(repoPath, dir) : repoPath;
96705
- if (!existsSync14(searchDir))
96838
+ if (!existsSync15(searchDir))
96706
96839
  continue;
96707
96840
  try {
96708
96841
  const files = walkDir(searchDir);
@@ -96714,7 +96847,7 @@ function findSpecFiles(repoPath, globPatterns) {
96714
96847
  seen.add(relativePath);
96715
96848
  const content = readFileSync6(file, "utf-8");
96716
96849
  const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
96717
- const stat = statSync(file);
96850
+ const stat = statSync2(file);
96718
96851
  specs.push({
96719
96852
  file: relativePath,
96720
96853
  fromGlob: pattern,
@@ -96754,7 +96887,7 @@ function matchesGlob(filePath, pattern) {
96754
96887
  }
96755
96888
  function detectSuggestedUrl(repoPath) {
96756
96889
  const pkgPath = join17(repoPath, "package.json");
96757
- if (!existsSync14(pkgPath))
96890
+ if (!existsSync15(pkgPath))
96758
96891
  return null;
96759
96892
  try {
96760
96893
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
@@ -96774,10 +96907,10 @@ function detectSuggestedUrl(repoPath) {
96774
96907
  }
96775
96908
  function checkPlaywrightBrowserInstalled(repoPath) {
96776
96909
  const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
96777
- if (existsSync14(cacheDir))
96910
+ if (existsSync15(cacheDir))
96778
96911
  return true;
96779
96912
  const globalCache = join17(repoPath, ".cache", "ms-playwright");
96780
- if (existsSync14(globalCache))
96913
+ if (existsSync15(globalCache))
96781
96914
  return true;
96782
96915
  return false;
96783
96916
  }
@@ -96817,7 +96950,7 @@ function discoverRepo(opts) {
96817
96950
  const specs = findSpecFiles(repoPath, globPatterns);
96818
96951
  const packageManager = detectPackageManager(repoPath);
96819
96952
  const devScripts = detectDevScripts(repoPath);
96820
- const playwrightInstalled = existsSync14(join17(repoPath, "node_modules", "playwright")) || existsSync14(join17(repoPath, "node_modules", "@playwright", "test"));
96953
+ const playwrightInstalled = existsSync15(join17(repoPath, "node_modules", "playwright")) || existsSync15(join17(repoPath, "node_modules", "@playwright", "test"));
96821
96954
  const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
96822
96955
  const configExists = configPath !== null;
96823
96956
  const specsFound = specs.length > 0;
@@ -96876,11 +97009,11 @@ function discoverRepo(opts) {
96876
97009
  }
96877
97010
  function clearDiscoveryCache(repoPath) {
96878
97011
  const cacheDir = getCacheDir();
96879
- if (!existsSync14(cacheDir))
97012
+ if (!existsSync15(cacheDir))
96880
97013
  return;
96881
97014
  if (repoPath) {
96882
97015
  const cachePath = getCachePath(repoPath);
96883
- if (existsSync14(cachePath)) {
97016
+ if (existsSync15(cachePath)) {
96884
97017
  unlinkSync(cachePath);
96885
97018
  }
96886
97019
  } else {
@@ -96893,7 +97026,7 @@ function clearDiscoveryCache(repoPath) {
96893
97026
  }
96894
97027
  function getDiscoveryCacheInfo(repoPath) {
96895
97028
  const cachePath = getCachePath(repoPath);
96896
- if (!existsSync14(cachePath))
97029
+ if (!existsSync15(cachePath))
96897
97030
  return null;
96898
97031
  const cached = loadCache(repoPath);
96899
97032
  if (!cached)
@@ -96910,11 +97043,11 @@ init_runs();
96910
97043
  init_database();
96911
97044
  init_paths();
96912
97045
  import { execSync as execSync2 } from "child_process";
96913
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "fs";
97046
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync5 } from "fs";
96914
97047
  import { join as join18 } from "path";
96915
97048
  function resolvePlaywrightCmd(repoPath) {
96916
97049
  const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
96917
- if (existsSync15(localPw)) {
97050
+ if (existsSync16(localPw)) {
96918
97051
  return [localPw, "test"];
96919
97052
  }
96920
97053
  return ["npx", "playwright", "test"];
@@ -97110,9 +97243,9 @@ async function runRepoTests(opts) {
97110
97243
  const resultRecord = { id: resultId };
97111
97244
  if (result.stdout || result.stderr) {
97112
97245
  const reportersDir = join18(getTestersDir(), "repo-run-output");
97113
- mkdirSync12(reportersDir, { recursive: true });
97246
+ mkdirSync13(reportersDir, { recursive: true });
97114
97247
  const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
97115
- writeFileSync6(outputFile, `=== stdout ===
97248
+ writeFileSync5(outputFile, `=== stdout ===
97116
97249
  ${result.stdout}
97117
97250
 
97118
97251
  === stderr ===
@@ -97246,6 +97379,29 @@ function validateStoredAssertion(value) {
97246
97379
  }
97247
97380
  return describeStoredAssertion(value);
97248
97381
  }
97382
+ function envCredentialRef(value) {
97383
+ const trimmed = value?.trim();
97384
+ if (!trimmed)
97385
+ return;
97386
+ return trimmed.startsWith("$") ? trimmed : `$${trimmed}`;
97387
+ }
97388
+ function parseSandboxEnv(values) {
97389
+ if (!values?.length)
97390
+ return;
97391
+ const env = {};
97392
+ for (const value of values) {
97393
+ const trimmed = value.trim();
97394
+ if (!trimmed)
97395
+ continue;
97396
+ const separator = trimmed.indexOf("=");
97397
+ const key = separator >= 0 ? trimmed.slice(0, separator).trim() : trimmed;
97398
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
97399
+ throw new Error(`Invalid sandbox env var name: ${key || value}`);
97400
+ }
97401
+ env[key] = separator >= 0 ? trimmed.slice(separator + 1) : `$${key}`;
97402
+ }
97403
+ return Object.keys(env).length > 0 ? env : undefined;
97404
+ }
97249
97405
  function AddForm({ onComplete }) {
97250
97406
  const { exit } = useApp();
97251
97407
  const [state, setState] = useState({
@@ -97519,7 +97675,7 @@ program2.command("prod-debug <target>").description("Create a safe production de
97519
97675
  }, config2.prodDebug);
97520
97676
  const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
97521
97677
  if (opts.output) {
97522
- writeFileSync8(resolve4(opts.output), output + `
97678
+ writeFileSync7(resolve4(opts.output), output + `
97523
97679
  `);
97524
97680
  } else {
97525
97681
  log(output);
@@ -97529,7 +97685,7 @@ var CONFIG_DIR5 = getTestersDir();
97529
97685
  var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
97530
97686
  function getActiveProject() {
97531
97687
  try {
97532
- if (existsSync17(CONFIG_PATH4)) {
97688
+ if (existsSync18(CONFIG_PATH4)) {
97533
97689
  const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
97534
97690
  return raw.activeProject ?? undefined;
97535
97691
  }
@@ -97550,12 +97706,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97550
97706
  }, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
97551
97707
  acc.push(val);
97552
97708
  return acc;
97553
- }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
97709
+ }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--auth-preset <name>", "Attach email/password/loginPath from a named auth preset").option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
97554
97710
  acc.push(val);
97555
97711
  return acc;
97556
97712
  }, []).action(async (name21, opts) => {
97557
97713
  try {
97558
- const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.timeout || opts.template || opts.assert?.length;
97714
+ const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.authPreset || opts.timeout || opts.template || opts.assert?.length;
97559
97715
  if (!name21 && !hasFlags) {
97560
97716
  const projectId2 = resolveProject2(opts.project);
97561
97717
  await runInteractiveAdd(projectId2);
@@ -97580,6 +97736,11 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97580
97736
  }
97581
97737
  const assertions = opts.assert.map(parseAssertionString);
97582
97738
  const projectId = resolveProject2(opts.project);
97739
+ const authPreset = opts.authPreset ? getAuthPreset(opts.authPreset) : null;
97740
+ if (opts.authPreset && !authPreset) {
97741
+ logError(chalk6.red(`Auth preset not found: ${opts.authPreset}`));
97742
+ process.exit(1);
97743
+ }
97583
97744
  const scenario = createScenario({
97584
97745
  name: name21,
97585
97746
  description: opts.description || name21,
@@ -97588,7 +97749,12 @@ program2.command("add [name]").alias("create").description("Create a new test sc
97588
97749
  priority: opts.priority,
97589
97750
  model: opts.model,
97590
97751
  targetPath: opts.path,
97591
- requiresAuth: opts.auth,
97752
+ requiresAuth: opts.auth || Boolean(authPreset),
97753
+ authConfig: authPreset ? {
97754
+ email: authPreset.email,
97755
+ password: authPreset.password,
97756
+ loginPath: authPreset.loginPath
97757
+ } : undefined,
97592
97758
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97593
97759
  assertions: assertions.length > 0 ? assertions : undefined,
97594
97760
  projectId
@@ -98083,7 +98249,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98083
98249
  if (opts.json || opts.output) {
98084
98250
  const jsonOutput = formatJSON(run3, results2);
98085
98251
  if (opts.output) {
98086
- writeFileSync8(opts.output, jsonOutput, "utf-8");
98252
+ writeFileSync7(opts.output, jsonOutput, "utf-8");
98087
98253
  log(chalk6.green(`Results written to ${opts.output}`));
98088
98254
  }
98089
98255
  if (opts.json) {
@@ -98192,7 +98358,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
98192
98358
  if (opts.json || opts.output) {
98193
98359
  const jsonOutput = formatJSON(run2, results);
98194
98360
  if (opts.output) {
98195
- writeFileSync8(opts.output, jsonOutput, "utf-8");
98361
+ writeFileSync7(opts.output, jsonOutput, "utf-8");
98196
98362
  log(chalk6.green(`Results written to ${opts.output}`));
98197
98363
  }
98198
98364
  if (opts.json) {
@@ -98445,13 +98611,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98445
98611
  if (fmt === "json") {
98446
98612
  const outputPath = opts.output ?? "testers-export.json";
98447
98613
  const data = JSON.stringify(scenarios, null, 2);
98448
- writeFileSync8(outputPath, data, "utf-8");
98614
+ writeFileSync7(outputPath, data, "utf-8");
98449
98615
  log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
98450
98616
  return;
98451
98617
  }
98452
98618
  const outputDir = opts.output ?? ".";
98453
- if (!existsSync17(outputDir)) {
98454
- mkdirSync14(outputDir, { recursive: true });
98619
+ if (!existsSync18(outputDir)) {
98620
+ mkdirSync15(outputDir, { recursive: true });
98455
98621
  }
98456
98622
  for (const s2 of scenarios) {
98457
98623
  const lines = [];
@@ -98479,7 +98645,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
98479
98645
  }
98480
98646
  const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
98481
98647
  const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
98482
- writeFileSync8(filePath, lines.join(`
98648
+ writeFileSync7(filePath, lines.join(`
98483
98649
  `), "utf-8");
98484
98650
  log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
98485
98651
  }
@@ -98653,17 +98819,17 @@ projectCmd.command("export-open <id>").description("Register a testers project i
98653
98819
  projectCmd.command("use <name>").description("Set active project (find or create)").option("--json", "Output as JSON", false).action((name21, opts) => {
98654
98820
  try {
98655
98821
  const project = ensureProject(name21, process.cwd());
98656
- if (!existsSync17(CONFIG_DIR5)) {
98657
- mkdirSync14(CONFIG_DIR5, { recursive: true });
98822
+ if (!existsSync18(CONFIG_DIR5)) {
98823
+ mkdirSync15(CONFIG_DIR5, { recursive: true });
98658
98824
  }
98659
98825
  let config2 = {};
98660
- if (existsSync17(CONFIG_PATH4)) {
98826
+ if (existsSync18(CONFIG_PATH4)) {
98661
98827
  try {
98662
98828
  config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
98663
98829
  } catch {}
98664
98830
  }
98665
98831
  config2.activeProject = project.id;
98666
- writeFileSync8(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
98832
+ writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
98667
98833
  if (opts.json) {
98668
98834
  log(JSON.stringify({ activeProject: project.id, project }, null, 2));
98669
98835
  return;
@@ -99272,10 +99438,10 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
99272
99438
  if (opts.output) {
99273
99439
  const outPath = resolve4(opts.output);
99274
99440
  const outDir = outPath.replace(/\/[^/]*$/, "");
99275
- if (outDir && !existsSync17(outDir)) {
99276
- mkdirSync14(outDir, { recursive: true });
99441
+ if (outDir && !existsSync18(outDir)) {
99442
+ mkdirSync15(outDir, { recursive: true });
99277
99443
  }
99278
- writeFileSync8(outPath, workflow, "utf-8");
99444
+ writeFileSync7(outPath, workflow, "utf-8");
99279
99445
  log(chalk6.green(`Workflow written to ${outPath}`));
99280
99446
  return;
99281
99447
  }
@@ -99307,11 +99473,11 @@ program2.command("init").description("Initialize a new testing project").option(
99307
99473
  }
99308
99474
  if (opts.ci === "github") {
99309
99475
  const workflowDir = join20(process.cwd(), ".github", "workflows");
99310
- if (!existsSync17(workflowDir)) {
99311
- mkdirSync14(workflowDir, { recursive: true });
99476
+ if (!existsSync18(workflowDir)) {
99477
+ mkdirSync15(workflowDir, { recursive: true });
99312
99478
  }
99313
99479
  const workflowPath = join20(workflowDir, "testers.yml");
99314
- writeFileSync8(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
99480
+ writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
99315
99481
  log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
99316
99482
  } else if (opts.ci) {
99317
99483
  log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
@@ -99504,7 +99670,7 @@ program2.command("quick-qa <url>").alias("quick-check").description("Run a fast
99504
99670
  wcagLevel
99505
99671
  });
99506
99672
  if (opts.output) {
99507
- writeFileSync8(resolve4(opts.output), JSON.stringify(result, null, 2));
99673
+ writeFileSync7(resolve4(opts.output), JSON.stringify(result, null, 2));
99508
99674
  }
99509
99675
  if (opts.json) {
99510
99676
  log(JSON.stringify(result, null, 2));
@@ -99555,7 +99721,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99555
99721
  format
99556
99722
  });
99557
99723
  if (opts.output && opts.output !== "report.html") {
99558
- writeFileSync8(opts.output, content, "utf-8");
99724
+ writeFileSync7(opts.output, content, "utf-8");
99559
99725
  const absPath2 = resolve4(opts.output);
99560
99726
  log(chalk6.green(`Compliance report written to ${absPath2}`));
99561
99727
  } else {
@@ -99569,7 +99735,7 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99569
99735
  } else {
99570
99736
  html = generateHtmlReport(runId);
99571
99737
  }
99572
- writeFileSync8(opts.output, html, "utf-8");
99738
+ writeFileSync7(opts.output, html, "utf-8");
99573
99739
  const absPath = resolve4(opts.output);
99574
99740
  log(chalk6.green(`Report generated: ${absPath}`));
99575
99741
  if (opts.open) {
@@ -99582,12 +99748,22 @@ program2.command("report [run-id]").description("Generate HTML test report or co
99582
99748
  }
99583
99749
  });
99584
99750
  var authCmd = program2.command("auth").description("Manage auth presets");
99585
- authCmd.command("add <name>").description("Create an auth preset").requiredOption("--email <email>", "Login email").requiredOption("--password <password>", "Login password").option("--login-path <path>", "Login page path", "/login").action((name21, opts) => {
99751
+ authCmd.command("add <name>").description("Create an auth preset").option("--email <email>", "Login email or credential reference").option("--password <password>", "Login password or credential reference").option("--email-env <name>", "Environment variable name for the login email").option("--password-env <name>", "Environment variable name for the login password").option("--login-path <path>", "Login page path", "/login").action((name21, opts) => {
99586
99752
  try {
99753
+ const email3 = opts.email ?? envCredentialRef(opts.emailEnv);
99754
+ const password = opts.password ?? envCredentialRef(opts.passwordEnv);
99755
+ if (!email3) {
99756
+ logError(chalk6.red("Error: provide --email or --email-env"));
99757
+ process.exit(1);
99758
+ }
99759
+ if (!password) {
99760
+ logError(chalk6.red("Error: provide --password or --password-env"));
99761
+ process.exit(1);
99762
+ }
99587
99763
  const preset = createAuthPreset({
99588
99764
  name: name21,
99589
- email: opts.email,
99590
- password: opts.password,
99765
+ email: email3,
99766
+ password,
99591
99767
  loginPath: opts.loginPath
99592
99768
  });
99593
99769
  log(chalk6.green(`Created auth preset ${chalk6.bold(preset.name)} (${preset.email})`));
@@ -100849,7 +101025,10 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100849
101025
  }, []).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
101026
  acc.push(val);
100851
101027
  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) => {
101028
+ }, []).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-env <assignment>", "Sandbox env var; KEY forwards host KEY, KEY=value stores value (repeatable)", (val, acc) => {
101029
+ acc.push(val);
101030
+ return acc;
101031
+ }, []).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
101032
  try {
100854
101033
  const workflow = createTestingWorkflow({
100855
101034
  name: name21,
@@ -100875,6 +101054,13 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
100875
101054
  sandboxSyncStrategy: opts.sandboxSync,
100876
101055
  setupCommand: opts.sandboxSetupCommand,
100877
101056
  packageSpec: opts.sandboxPackage,
101057
+ env: parseSandboxEnv(opts.sandboxEnv),
101058
+ appSourceDir: opts.sandboxAppSource,
101059
+ appRemoteDir: opts.sandboxAppRemoteDir,
101060
+ appStartCommand: opts.sandboxAppStartCommand,
101061
+ appUrl: opts.sandboxAppUrl,
101062
+ appWaitUrl: opts.sandboxAppWaitUrl,
101063
+ appWaitTimeoutMs: opts.sandboxAppWaitTimeout ? parseInt(opts.sandboxAppWaitTimeout, 10) : undefined,
100878
101064
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
100879
101065
  }
100880
101066
  });