@hasna/testers 0.0.42 → 0.0.44
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/README.md +21 -0
- package/dist/cli/index.js +359 -179
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/index.js +34 -7
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/workflow-fanout.d.ts +36 -0
- package/dist/lib/workflow-fanout.d.ts.map +1 -0
- package/dist/lib/workflow-runner.d.ts +3 -1
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/mcp/index.js +38 -10
- package/dist/server/index.js +36 -9
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2120,6 +2120,11 @@ function cleanupValue(value) {
|
|
|
2120
2120
|
return value;
|
|
2121
2121
|
return;
|
|
2122
2122
|
}
|
|
2123
|
+
function syncStrategyValue(value) {
|
|
2124
|
+
if (value === "archive" || value === "rsync")
|
|
2125
|
+
return value;
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2123
2128
|
function workflowExecutionFromValue(value) {
|
|
2124
2129
|
const input = isRecord(value) ? value : {};
|
|
2125
2130
|
const rawTarget = stringValue(input["target"]) ?? "local";
|
|
@@ -2134,6 +2139,7 @@ function workflowExecutionFromValue(value) {
|
|
|
2134
2139
|
const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
|
|
2135
2140
|
const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
|
|
2136
2141
|
const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
|
|
2142
|
+
const sandboxSyncStrategy = syncStrategyValue(input["sandboxSyncStrategy"]);
|
|
2137
2143
|
const setupCommand = stringValue(input["setupCommand"]);
|
|
2138
2144
|
const packageSpec = stringValue(input["packageSpec"]);
|
|
2139
2145
|
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
@@ -2144,6 +2150,7 @@ function workflowExecutionFromValue(value) {
|
|
|
2144
2150
|
...sandboxImage ? { sandboxImage } : {},
|
|
2145
2151
|
...sandboxRemoteDir ? { sandboxRemoteDir } : {},
|
|
2146
2152
|
...sandboxCleanup ? { sandboxCleanup } : {},
|
|
2153
|
+
...sandboxSyncStrategy ? { sandboxSyncStrategy } : {},
|
|
2147
2154
|
...setupCommand ? { setupCommand } : {},
|
|
2148
2155
|
...packageSpec ? { packageSpec } : {},
|
|
2149
2156
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
@@ -2175,6 +2182,8 @@ function projectFromRow(row) {
|
|
|
2175
2182
|
baseUrl: row.base_url ?? null,
|
|
2176
2183
|
port: row.port ?? null,
|
|
2177
2184
|
settings: row.settings ? JSON.parse(row.settings) : {},
|
|
2185
|
+
scenarioPrefix: row.scenario_prefix ?? "TST",
|
|
2186
|
+
scenarioCounter: row.scenario_counter ?? 0,
|
|
2178
2187
|
createdAt: row.created_at,
|
|
2179
2188
|
updatedAt: row.updated_at
|
|
2180
2189
|
};
|
|
@@ -12378,9 +12387,14 @@ function nextShortId(projectId) {
|
|
|
12378
12387
|
if (projectId) {
|
|
12379
12388
|
const project = db2.query("SELECT scenario_prefix, scenario_counter FROM projects WHERE id = ?").get(projectId);
|
|
12380
12389
|
if (project) {
|
|
12381
|
-
|
|
12390
|
+
let next = (project.scenario_counter ?? 0) + 1;
|
|
12391
|
+
let shortId = `${project.scenario_prefix || "TST"}-${next}`;
|
|
12392
|
+
while (db2.query("SELECT 1 FROM scenarios WHERE short_id = ?").get(shortId)) {
|
|
12393
|
+
next += 1;
|
|
12394
|
+
shortId = `${project.scenario_prefix || "TST"}-${next}`;
|
|
12395
|
+
}
|
|
12382
12396
|
db2.query("UPDATE projects SET scenario_counter = ? WHERE id = ?").run(next, projectId);
|
|
12383
|
-
return
|
|
12397
|
+
return shortId;
|
|
12384
12398
|
}
|
|
12385
12399
|
}
|
|
12386
12400
|
return shortUuid();
|
|
@@ -13934,6 +13948,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
|
|
|
13934
13948
|
function getDefaultConfig() {
|
|
13935
13949
|
return {
|
|
13936
13950
|
defaultModel: "claude-haiku-4-5-20251001",
|
|
13951
|
+
defaultImageModel: DEFAULT_IMAGE_MODEL,
|
|
13937
13952
|
models: { ...MODEL_MAP },
|
|
13938
13953
|
browser: {
|
|
13939
13954
|
headless: true,
|
|
@@ -13960,6 +13975,7 @@ function loadConfig() {
|
|
|
13960
13975
|
}
|
|
13961
13976
|
const config = {
|
|
13962
13977
|
defaultModel: fileConfig.defaultModel ?? defaults2.defaultModel,
|
|
13978
|
+
defaultImageModel: fileConfig.defaultImageModel ?? defaults2.defaultImageModel,
|
|
13963
13979
|
models: fileConfig.models ? { ...defaults2.models, ...fileConfig.models } : { ...defaults2.models },
|
|
13964
13980
|
browser: fileConfig.browser ? { ...defaults2.browser, ...fileConfig.browser } : { ...defaults2.browser },
|
|
13965
13981
|
screenshots: fileConfig.screenshots ? { ...defaults2.screenshots, ...fileConfig.screenshots } : { ...defaults2.screenshots },
|
|
@@ -13975,6 +13991,10 @@ function loadConfig() {
|
|
|
13975
13991
|
if (envModel) {
|
|
13976
13992
|
config.defaultModel = envModel;
|
|
13977
13993
|
}
|
|
13994
|
+
const envImageModel = process.env["TESTERS_IMAGE_MODEL"];
|
|
13995
|
+
if (envImageModel) {
|
|
13996
|
+
config.defaultImageModel = envImageModel;
|
|
13997
|
+
}
|
|
13978
13998
|
const envScreenshotsDir = process.env["TESTERS_SCREENSHOTS_DIR"];
|
|
13979
13999
|
if (envScreenshotsDir) {
|
|
13980
14000
|
config.screenshots.dir = envScreenshotsDir;
|
|
@@ -13989,7 +14009,7 @@ function loadConfig() {
|
|
|
13989
14009
|
}
|
|
13990
14010
|
return config;
|
|
13991
14011
|
}
|
|
13992
|
-
var CONFIG_DIR3, CONFIG_PATH2;
|
|
14012
|
+
var CONFIG_DIR3, CONFIG_PATH2, DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
|
13993
14013
|
var init_config2 = __esm(() => {
|
|
13994
14014
|
init_types();
|
|
13995
14015
|
init_paths();
|
|
@@ -19165,14 +19185,19 @@ var init_reporter = __esm(() => {
|
|
|
19165
19185
|
});
|
|
19166
19186
|
|
|
19167
19187
|
// src/db/projects.ts
|
|
19188
|
+
function normalizeScenarioPrefix(prefix) {
|
|
19189
|
+
const normalized = (prefix ?? "TST").trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
19190
|
+
return normalized || "TST";
|
|
19191
|
+
}
|
|
19168
19192
|
function createProject(input) {
|
|
19169
19193
|
const db2 = getDatabase();
|
|
19170
19194
|
const id = uuid();
|
|
19171
19195
|
const timestamp = now();
|
|
19196
|
+
const scenarioPrefix = normalizeScenarioPrefix(input.scenarioPrefix);
|
|
19172
19197
|
db2.query(`
|
|
19173
|
-
INSERT INTO projects (id, name, path, description, base_url, port, settings, created_at, updated_at)
|
|
19174
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
19175
|
-
`).run(id, input.name, input.path ?? null, input.description ?? null, input.baseUrl ?? null, input.port ?? null, input.settings ? JSON.stringify(input.settings) : "{}", timestamp, timestamp);
|
|
19198
|
+
INSERT INTO projects (id, name, path, description, base_url, port, settings, scenario_prefix, scenario_counter, created_at, updated_at)
|
|
19199
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
19200
|
+
`).run(id, input.name, input.path ?? null, input.description ?? null, input.baseUrl ?? null, input.port ?? null, input.settings ? JSON.stringify(input.settings) : "{}", scenarioPrefix, timestamp, timestamp);
|
|
19176
19201
|
return getProject(id);
|
|
19177
19202
|
}
|
|
19178
19203
|
function getProject(id) {
|
|
@@ -27001,6 +27026,178 @@ var init_workflows = __esm(() => {
|
|
|
27001
27026
|
DEFAULT_EXECUTION = { target: "local" };
|
|
27002
27027
|
});
|
|
27003
27028
|
|
|
27029
|
+
// src/lib/workflow-runner.ts
|
|
27030
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
27031
|
+
import { tmpdir } from "os";
|
|
27032
|
+
import { join as join16 } from "path";
|
|
27033
|
+
function buildWorkflowRunPlan(workflow, options) {
|
|
27034
|
+
const runOptions = {
|
|
27035
|
+
url: options.url,
|
|
27036
|
+
model: options.model,
|
|
27037
|
+
headed: options.headed,
|
|
27038
|
+
parallel: options.parallel,
|
|
27039
|
+
timeout: options.timeout ?? workflow.execution.timeoutMs,
|
|
27040
|
+
projectId: workflow.projectId ?? undefined,
|
|
27041
|
+
scenarioIds: workflow.scenarioFilter.scenarioIds,
|
|
27042
|
+
tags: workflow.scenarioFilter.tags,
|
|
27043
|
+
priority: workflow.scenarioFilter.priority,
|
|
27044
|
+
personaIds: workflow.personaIds.length > 0 ? workflow.personaIds : undefined
|
|
27045
|
+
};
|
|
27046
|
+
return {
|
|
27047
|
+
workflow,
|
|
27048
|
+
runOptions,
|
|
27049
|
+
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
27050
|
+
};
|
|
27051
|
+
}
|
|
27052
|
+
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
27053
|
+
const workflow = getTestingWorkflow(workflowId);
|
|
27054
|
+
if (!workflow)
|
|
27055
|
+
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
27056
|
+
if (!workflow.enabled)
|
|
27057
|
+
throw new Error(`Testing workflow is disabled: ${workflow.name}`);
|
|
27058
|
+
validatePersonaIds(workflow);
|
|
27059
|
+
const plan = buildWorkflowRunPlan(workflow, options);
|
|
27060
|
+
if (options.dryRun)
|
|
27061
|
+
return { run: null, results: [], plan };
|
|
27062
|
+
if (workflow.execution.target === "sandbox") {
|
|
27063
|
+
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
27064
|
+
return { run: null, results: [], plan, sandboxResult };
|
|
27065
|
+
}
|
|
27066
|
+
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
27067
|
+
const { run, results } = await runLocal(plan.runOptions);
|
|
27068
|
+
return { run, results, plan };
|
|
27069
|
+
}
|
|
27070
|
+
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
27071
|
+
if (!plan.sandbox)
|
|
27072
|
+
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
27073
|
+
const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
27074
|
+
writeFileSync4(join16(localDir, "testers.db"), getDatabase().serialize());
|
|
27075
|
+
return {
|
|
27076
|
+
localDir,
|
|
27077
|
+
remoteDir: plan.sandbox.stateRemoteDir,
|
|
27078
|
+
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
27079
|
+
};
|
|
27080
|
+
}
|
|
27081
|
+
function validatePersonaIds(workflow) {
|
|
27082
|
+
for (const personaId of workflow.personaIds) {
|
|
27083
|
+
if (!getPersona(personaId)) {
|
|
27084
|
+
throw new Error(`Persona not found for workflow ${workflow.name}: ${personaId}`);
|
|
27085
|
+
}
|
|
27086
|
+
}
|
|
27087
|
+
}
|
|
27088
|
+
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
27089
|
+
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
27090
|
+
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
27091
|
+
return {
|
|
27092
|
+
provider: execution.provider,
|
|
27093
|
+
image: execution.sandboxImage,
|
|
27094
|
+
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
27095
|
+
remoteDir,
|
|
27096
|
+
stateRemoteDir,
|
|
27097
|
+
cleanup: execution.sandboxCleanup ?? "delete",
|
|
27098
|
+
syncStrategy: execution.sandboxSyncStrategy ?? "rsync",
|
|
27099
|
+
timeoutMs: execution.timeoutMs,
|
|
27100
|
+
env: execution.env,
|
|
27101
|
+
command: buildSandboxCommand({
|
|
27102
|
+
runOptions,
|
|
27103
|
+
remoteDir,
|
|
27104
|
+
dbPath: `${stateRemoteDir}/testers.db`,
|
|
27105
|
+
setupCommand: execution.setupCommand,
|
|
27106
|
+
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
27107
|
+
})
|
|
27108
|
+
};
|
|
27109
|
+
}
|
|
27110
|
+
function buildSandboxCommand(input) {
|
|
27111
|
+
const args = [
|
|
27112
|
+
"bunx",
|
|
27113
|
+
input.packageSpec,
|
|
27114
|
+
"run",
|
|
27115
|
+
input.runOptions.url,
|
|
27116
|
+
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
27117
|
+
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
27118
|
+
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
27119
|
+
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
27120
|
+
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
27121
|
+
...input.runOptions.headed ? ["--headed"] : [],
|
|
27122
|
+
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
27123
|
+
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
27124
|
+
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
27125
|
+
"--no-auto-generate",
|
|
27126
|
+
"--json"
|
|
27127
|
+
];
|
|
27128
|
+
return [
|
|
27129
|
+
"set -euo pipefail",
|
|
27130
|
+
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
27131
|
+
`cd ${shellQuote(input.remoteDir)}`,
|
|
27132
|
+
input.setupCommand,
|
|
27133
|
+
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
27134
|
+
].filter(Boolean).join(`
|
|
27135
|
+
`);
|
|
27136
|
+
}
|
|
27137
|
+
async function runViaSandbox(plan, dependencies) {
|
|
27138
|
+
if (!plan.sandbox)
|
|
27139
|
+
throw new Error("Workflow does not have a sandbox plan");
|
|
27140
|
+
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
27141
|
+
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
27142
|
+
const bundle = createBundle(plan.workflow, plan);
|
|
27143
|
+
try {
|
|
27144
|
+
const raw = await sandboxes.runCommandInSandbox({
|
|
27145
|
+
command: plan.sandbox.command,
|
|
27146
|
+
provider: plan.sandbox.provider,
|
|
27147
|
+
name: plan.sandbox.name,
|
|
27148
|
+
image: plan.sandbox.image,
|
|
27149
|
+
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
27150
|
+
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
27151
|
+
projectId: plan.workflow.projectId ?? undefined,
|
|
27152
|
+
config: {
|
|
27153
|
+
source: "testers",
|
|
27154
|
+
workflowId: plan.workflow.id,
|
|
27155
|
+
workflowName: plan.workflow.name
|
|
27156
|
+
},
|
|
27157
|
+
sandboxEnvVars: plan.sandbox.env,
|
|
27158
|
+
cleanup: plan.sandbox.cleanup,
|
|
27159
|
+
upload: {
|
|
27160
|
+
localDir: bundle.localDir,
|
|
27161
|
+
remoteDir: bundle.remoteDir,
|
|
27162
|
+
syncStrategy: plan.sandbox.syncStrategy
|
|
27163
|
+
}
|
|
27164
|
+
});
|
|
27165
|
+
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
27166
|
+
const stdout = raw.result.stdout ?? "";
|
|
27167
|
+
const stderr = raw.result.stderr ?? "";
|
|
27168
|
+
if (exitCode !== 0) {
|
|
27169
|
+
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
27170
|
+
}
|
|
27171
|
+
return {
|
|
27172
|
+
sandboxId: raw.sandbox.id,
|
|
27173
|
+
sessionId: raw.session.id,
|
|
27174
|
+
exitCode,
|
|
27175
|
+
stdout,
|
|
27176
|
+
stderr,
|
|
27177
|
+
cleanup: raw.cleanup
|
|
27178
|
+
};
|
|
27179
|
+
} finally {
|
|
27180
|
+
bundle.cleanup?.();
|
|
27181
|
+
}
|
|
27182
|
+
}
|
|
27183
|
+
async function resolveSandboxesRuntime(dependencies) {
|
|
27184
|
+
if (dependencies.sandboxes)
|
|
27185
|
+
return dependencies.sandboxes;
|
|
27186
|
+
if (dependencies.createSandboxesSDK)
|
|
27187
|
+
return dependencies.createSandboxesSDK();
|
|
27188
|
+
const mod = await import("@hasna/sandboxes");
|
|
27189
|
+
return mod.createSandboxesSDK();
|
|
27190
|
+
}
|
|
27191
|
+
function shellQuote(value) {
|
|
27192
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
27193
|
+
}
|
|
27194
|
+
var init_workflow_runner = __esm(() => {
|
|
27195
|
+
init_database();
|
|
27196
|
+
init_workflows();
|
|
27197
|
+
init_personas();
|
|
27198
|
+
init_runner();
|
|
27199
|
+
});
|
|
27200
|
+
|
|
27004
27201
|
// src/lib/ci.ts
|
|
27005
27202
|
var exports_ci = {};
|
|
27006
27203
|
__export(exports_ci, {
|
|
@@ -59383,6 +59580,115 @@ var init_agents = __esm(() => {
|
|
|
59383
59580
|
init_database();
|
|
59384
59581
|
});
|
|
59385
59582
|
|
|
59583
|
+
// src/lib/workflow-fanout.ts
|
|
59584
|
+
var exports_workflow_fanout = {};
|
|
59585
|
+
__export(exports_workflow_fanout, {
|
|
59586
|
+
runWorkflowFanout: () => runWorkflowFanout,
|
|
59587
|
+
resolveWorkflowFanoutSelection: () => resolveWorkflowFanoutSelection,
|
|
59588
|
+
normalizeFanoutWorkerCount: () => normalizeFanoutWorkerCount
|
|
59589
|
+
});
|
|
59590
|
+
function splitWorkflowIds(ids) {
|
|
59591
|
+
return (ids ?? []).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
59592
|
+
}
|
|
59593
|
+
function normalizeFanoutWorkerCount(value) {
|
|
59594
|
+
const workers = Math.floor(value ?? 6);
|
|
59595
|
+
if (!Number.isFinite(workers) || workers < 1 || workers > 12) {
|
|
59596
|
+
throw new Error("workflow fanout workers must be between 1 and 12");
|
|
59597
|
+
}
|
|
59598
|
+
return workers;
|
|
59599
|
+
}
|
|
59600
|
+
function resolveWorkflowFanoutSelection(options) {
|
|
59601
|
+
const ids = splitWorkflowIds(options.workflowIds);
|
|
59602
|
+
const workflows = ids.length > 0 ? ids.map((id) => {
|
|
59603
|
+
const workflow = getTestingWorkflow(id);
|
|
59604
|
+
if (!workflow)
|
|
59605
|
+
throw new Error(`Testing workflow not found: ${id}`);
|
|
59606
|
+
return workflow;
|
|
59607
|
+
}) : listTestingWorkflows({
|
|
59608
|
+
projectId: options.projectId,
|
|
59609
|
+
enabled: options.includeDisabled ? undefined : true
|
|
59610
|
+
});
|
|
59611
|
+
const tagSet = new Set(options.tags ?? []);
|
|
59612
|
+
const filtered = tagSet.size === 0 ? workflows : workflows.filter((workflow) => workflow.scenarioFilter.tags?.some((tag) => tagSet.has(tag)));
|
|
59613
|
+
if (filtered.length === 0) {
|
|
59614
|
+
throw new Error("No testing workflows matched the fanout selection");
|
|
59615
|
+
}
|
|
59616
|
+
const nonSandbox = filtered.filter((workflow) => workflow.execution.target !== "sandbox");
|
|
59617
|
+
if (nonSandbox.length > 0) {
|
|
59618
|
+
throw new Error(`workflow fanout requires sandbox workflows. Recreate or update these with --target sandbox: ${nonSandbox.map((workflow) => workflow.name).join(", ")}`);
|
|
59619
|
+
}
|
|
59620
|
+
return filtered;
|
|
59621
|
+
}
|
|
59622
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
59623
|
+
const output = new Array(items.length);
|
|
59624
|
+
let next = 0;
|
|
59625
|
+
async function runWorker() {
|
|
59626
|
+
while (next < items.length) {
|
|
59627
|
+
const index = next++;
|
|
59628
|
+
output[index] = await worker(items[index], index);
|
|
59629
|
+
}
|
|
59630
|
+
}
|
|
59631
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => runWorker()));
|
|
59632
|
+
return output;
|
|
59633
|
+
}
|
|
59634
|
+
async function runWorkflowFanout(options, dependencies = {}) {
|
|
59635
|
+
const workers = normalizeFanoutWorkerCount(options.workers);
|
|
59636
|
+
const workflows = resolveWorkflowFanoutSelection(options);
|
|
59637
|
+
const { runTestingWorkflow: runOne = runTestingWorkflow, ...workflowDependencies } = dependencies;
|
|
59638
|
+
const items = await mapWithConcurrency(workflows, workers, async (workflow) => {
|
|
59639
|
+
try {
|
|
59640
|
+
const output = await runOne(workflow.id, {
|
|
59641
|
+
url: options.url,
|
|
59642
|
+
model: options.model,
|
|
59643
|
+
headed: options.headed,
|
|
59644
|
+
parallel: options.parallel,
|
|
59645
|
+
timeout: options.timeout,
|
|
59646
|
+
dryRun: options.dryRun
|
|
59647
|
+
}, workflowDependencies);
|
|
59648
|
+
if (options.dryRun) {
|
|
59649
|
+
return {
|
|
59650
|
+
workflowId: workflow.id,
|
|
59651
|
+
workflowName: workflow.name,
|
|
59652
|
+
status: "dry-run",
|
|
59653
|
+
plan: output.plan
|
|
59654
|
+
};
|
|
59655
|
+
}
|
|
59656
|
+
return {
|
|
59657
|
+
workflowId: workflow.id,
|
|
59658
|
+
workflowName: workflow.name,
|
|
59659
|
+
status: "passed",
|
|
59660
|
+
sandboxId: output.sandboxResult?.sandboxId,
|
|
59661
|
+
sessionId: output.sandboxResult?.sessionId,
|
|
59662
|
+
exitCode: output.sandboxResult?.exitCode,
|
|
59663
|
+
stdout: output.sandboxResult?.stdout,
|
|
59664
|
+
stderr: output.sandboxResult?.stderr
|
|
59665
|
+
};
|
|
59666
|
+
} catch (error) {
|
|
59667
|
+
return {
|
|
59668
|
+
workflowId: workflow.id,
|
|
59669
|
+
workflowName: workflow.name,
|
|
59670
|
+
status: "failed",
|
|
59671
|
+
error: error instanceof Error ? error.message : String(error)
|
|
59672
|
+
};
|
|
59673
|
+
}
|
|
59674
|
+
});
|
|
59675
|
+
const failed = items.filter((item) => item.status === "failed").length;
|
|
59676
|
+
const passed = items.filter((item) => item.status === "passed").length;
|
|
59677
|
+
const dryRun = options.dryRun === true;
|
|
59678
|
+
return {
|
|
59679
|
+
status: dryRun ? "dry-run" : failed > 0 ? "failed" : "passed",
|
|
59680
|
+
workers,
|
|
59681
|
+
total: items.length,
|
|
59682
|
+
passed,
|
|
59683
|
+
failed,
|
|
59684
|
+
items
|
|
59685
|
+
};
|
|
59686
|
+
}
|
|
59687
|
+
var init_workflow_fanout = __esm(() => {
|
|
59688
|
+
init_workflows();
|
|
59689
|
+
init_workflow_runner();
|
|
59690
|
+
});
|
|
59691
|
+
|
|
59386
59692
|
// node_modules/@ai-sdk/provider/dist/index.mjs
|
|
59387
59693
|
function getErrorMessage(error) {
|
|
59388
59694
|
if (error == null) {
|
|
@@ -94040,7 +94346,7 @@ import chalk6 from "chalk";
|
|
|
94040
94346
|
// package.json
|
|
94041
94347
|
var package_default = {
|
|
94042
94348
|
name: "@hasna/testers",
|
|
94043
|
-
version: "0.0.
|
|
94349
|
+
version: "0.0.44",
|
|
94044
94350
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
94045
94351
|
type: "module",
|
|
94046
94352
|
main: "dist/index.js",
|
|
@@ -94086,7 +94392,7 @@ var package_default = {
|
|
|
94086
94392
|
"@hasna/cloud": "^0.1.24",
|
|
94087
94393
|
"@hasna/contacts": "^0.6.8",
|
|
94088
94394
|
"@hasna/projects": "^0.1.42",
|
|
94089
|
-
"@hasna/sandboxes": "^0.1.
|
|
94395
|
+
"@hasna/sandboxes": "^0.1.28",
|
|
94090
94396
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
94091
94397
|
ai: "^6.0.175",
|
|
94092
94398
|
chalk: "^5.4.1",
|
|
@@ -96152,174 +96458,7 @@ function deleteAuthPreset(name) {
|
|
|
96152
96458
|
// src/cli/index.tsx
|
|
96153
96459
|
init_flows();
|
|
96154
96460
|
init_workflows();
|
|
96155
|
-
|
|
96156
|
-
// src/lib/workflow-runner.ts
|
|
96157
|
-
init_database();
|
|
96158
|
-
init_workflows();
|
|
96159
|
-
init_personas();
|
|
96160
|
-
init_runner();
|
|
96161
|
-
import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
96162
|
-
import { tmpdir } from "os";
|
|
96163
|
-
import { join as join16 } from "path";
|
|
96164
|
-
function buildWorkflowRunPlan(workflow, options) {
|
|
96165
|
-
const runOptions = {
|
|
96166
|
-
url: options.url,
|
|
96167
|
-
model: options.model,
|
|
96168
|
-
headed: options.headed,
|
|
96169
|
-
parallel: options.parallel,
|
|
96170
|
-
timeout: options.timeout ?? workflow.execution.timeoutMs,
|
|
96171
|
-
projectId: workflow.projectId ?? undefined,
|
|
96172
|
-
scenarioIds: workflow.scenarioFilter.scenarioIds,
|
|
96173
|
-
tags: workflow.scenarioFilter.tags,
|
|
96174
|
-
priority: workflow.scenarioFilter.priority,
|
|
96175
|
-
personaIds: workflow.personaIds.length > 0 ? workflow.personaIds : undefined
|
|
96176
|
-
};
|
|
96177
|
-
return {
|
|
96178
|
-
workflow,
|
|
96179
|
-
runOptions,
|
|
96180
|
-
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
96181
|
-
};
|
|
96182
|
-
}
|
|
96183
|
-
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
96184
|
-
const workflow = getTestingWorkflow(workflowId);
|
|
96185
|
-
if (!workflow)
|
|
96186
|
-
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
96187
|
-
if (!workflow.enabled)
|
|
96188
|
-
throw new Error(`Testing workflow is disabled: ${workflow.name}`);
|
|
96189
|
-
validatePersonaIds(workflow);
|
|
96190
|
-
const plan = buildWorkflowRunPlan(workflow, options);
|
|
96191
|
-
if (options.dryRun)
|
|
96192
|
-
return { run: null, results: [], plan };
|
|
96193
|
-
if (workflow.execution.target === "sandbox") {
|
|
96194
|
-
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
96195
|
-
return { run: null, results: [], plan, sandboxResult };
|
|
96196
|
-
}
|
|
96197
|
-
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
96198
|
-
const { run, results } = await runLocal(plan.runOptions);
|
|
96199
|
-
return { run, results, plan };
|
|
96200
|
-
}
|
|
96201
|
-
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
96202
|
-
if (!plan.sandbox)
|
|
96203
|
-
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
96204
|
-
const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
96205
|
-
writeFileSync4(join16(localDir, "testers.db"), getDatabase().serialize());
|
|
96206
|
-
return {
|
|
96207
|
-
localDir,
|
|
96208
|
-
remoteDir: plan.sandbox.stateRemoteDir,
|
|
96209
|
-
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
96210
|
-
};
|
|
96211
|
-
}
|
|
96212
|
-
function validatePersonaIds(workflow) {
|
|
96213
|
-
for (const personaId of workflow.personaIds) {
|
|
96214
|
-
if (!getPersona(personaId)) {
|
|
96215
|
-
throw new Error(`Persona not found for workflow ${workflow.name}: ${personaId}`);
|
|
96216
|
-
}
|
|
96217
|
-
}
|
|
96218
|
-
}
|
|
96219
|
-
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
96220
|
-
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
96221
|
-
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
96222
|
-
return {
|
|
96223
|
-
provider: execution.provider,
|
|
96224
|
-
image: execution.sandboxImage,
|
|
96225
|
-
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
96226
|
-
remoteDir,
|
|
96227
|
-
stateRemoteDir,
|
|
96228
|
-
cleanup: execution.sandboxCleanup ?? "delete",
|
|
96229
|
-
timeoutMs: execution.timeoutMs,
|
|
96230
|
-
env: execution.env,
|
|
96231
|
-
command: buildSandboxCommand({
|
|
96232
|
-
runOptions,
|
|
96233
|
-
remoteDir,
|
|
96234
|
-
dbPath: `${stateRemoteDir}/testers.db`,
|
|
96235
|
-
setupCommand: execution.setupCommand,
|
|
96236
|
-
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
96237
|
-
})
|
|
96238
|
-
};
|
|
96239
|
-
}
|
|
96240
|
-
function buildSandboxCommand(input) {
|
|
96241
|
-
const args = [
|
|
96242
|
-
"bunx",
|
|
96243
|
-
input.packageSpec,
|
|
96244
|
-
"run",
|
|
96245
|
-
input.runOptions.url,
|
|
96246
|
-
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
96247
|
-
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
96248
|
-
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
96249
|
-
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
96250
|
-
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
96251
|
-
...input.runOptions.headed ? ["--headed"] : [],
|
|
96252
|
-
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
96253
|
-
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
96254
|
-
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
96255
|
-
"--no-auto-generate",
|
|
96256
|
-
"--json"
|
|
96257
|
-
];
|
|
96258
|
-
return [
|
|
96259
|
-
"set -euo pipefail",
|
|
96260
|
-
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
96261
|
-
`cd ${shellQuote(input.remoteDir)}`,
|
|
96262
|
-
input.setupCommand,
|
|
96263
|
-
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
96264
|
-
].filter(Boolean).join(`
|
|
96265
|
-
`);
|
|
96266
|
-
}
|
|
96267
|
-
async function runViaSandbox(plan, dependencies) {
|
|
96268
|
-
if (!plan.sandbox)
|
|
96269
|
-
throw new Error("Workflow does not have a sandbox plan");
|
|
96270
|
-
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
96271
|
-
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
96272
|
-
const bundle = createBundle(plan.workflow, plan);
|
|
96273
|
-
try {
|
|
96274
|
-
const raw = await sandboxes.runCommandInSandbox({
|
|
96275
|
-
command: plan.sandbox.command,
|
|
96276
|
-
provider: plan.sandbox.provider,
|
|
96277
|
-
name: plan.sandbox.name,
|
|
96278
|
-
image: plan.sandbox.image,
|
|
96279
|
-
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
96280
|
-
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
96281
|
-
projectId: plan.workflow.projectId ?? undefined,
|
|
96282
|
-
config: {
|
|
96283
|
-
source: "testers",
|
|
96284
|
-
workflowId: plan.workflow.id,
|
|
96285
|
-
workflowName: plan.workflow.name
|
|
96286
|
-
},
|
|
96287
|
-
sandboxEnvVars: plan.sandbox.env,
|
|
96288
|
-
cleanup: plan.sandbox.cleanup,
|
|
96289
|
-
upload: {
|
|
96290
|
-
localDir: bundle.localDir,
|
|
96291
|
-
remoteDir: bundle.remoteDir
|
|
96292
|
-
}
|
|
96293
|
-
});
|
|
96294
|
-
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
96295
|
-
const stdout = raw.result.stdout ?? "";
|
|
96296
|
-
const stderr = raw.result.stderr ?? "";
|
|
96297
|
-
if (exitCode !== 0) {
|
|
96298
|
-
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
96299
|
-
}
|
|
96300
|
-
return {
|
|
96301
|
-
sandboxId: raw.sandbox.id,
|
|
96302
|
-
sessionId: raw.session.id,
|
|
96303
|
-
exitCode,
|
|
96304
|
-
stdout,
|
|
96305
|
-
stderr,
|
|
96306
|
-
cleanup: raw.cleanup
|
|
96307
|
-
};
|
|
96308
|
-
} finally {
|
|
96309
|
-
bundle.cleanup?.();
|
|
96310
|
-
}
|
|
96311
|
-
}
|
|
96312
|
-
async function resolveSandboxesRuntime(dependencies) {
|
|
96313
|
-
if (dependencies.sandboxes)
|
|
96314
|
-
return dependencies.sandboxes;
|
|
96315
|
-
if (dependencies.createSandboxesSDK)
|
|
96316
|
-
return dependencies.createSandboxesSDK();
|
|
96317
|
-
const mod = await import("@hasna/sandboxes");
|
|
96318
|
-
return mod.createSandboxesSDK();
|
|
96319
|
-
}
|
|
96320
|
-
function shellQuote(value) {
|
|
96321
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
96322
|
-
}
|
|
96461
|
+
init_workflow_runner();
|
|
96323
96462
|
|
|
96324
96463
|
// src/db/environments.ts
|
|
96325
96464
|
init_database();
|
|
@@ -98350,6 +98489,7 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
98350
98489
|
log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk6.green("set") : chalk6.red("not set")}`);
|
|
98351
98490
|
log(` Database: ${dbPath}`);
|
|
98352
98491
|
log(` Default model: ${config2.defaultModel}`);
|
|
98492
|
+
log(` Image model: ${config2.defaultImageModel}`);
|
|
98353
98493
|
log(` Screenshots dir: ${config2.screenshots.dir}`);
|
|
98354
98494
|
log("");
|
|
98355
98495
|
} catch (error40) {
|
|
@@ -98380,7 +98520,8 @@ projectCmd.command("create <name>").description("Create a new project").option("
|
|
|
98380
98520
|
const project = createProject({
|
|
98381
98521
|
name: name21,
|
|
98382
98522
|
path: opts.path,
|
|
98383
|
-
description: opts.description
|
|
98523
|
+
description: opts.description,
|
|
98524
|
+
scenarioPrefix: opts.prefix
|
|
98384
98525
|
});
|
|
98385
98526
|
log(chalk6.green(`Created project ${chalk6.bold(project.name)} (${project.id})`));
|
|
98386
98527
|
} catch (error40) {
|
|
@@ -100687,7 +100828,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100687
100828
|
}, []).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) => {
|
|
100688
100829
|
acc.push(val);
|
|
100689
100830
|
return acc;
|
|
100690
|
-
}, []).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-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) => {
|
|
100831
|
+
}, []).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) => {
|
|
100691
100832
|
try {
|
|
100692
100833
|
const workflow = createTestingWorkflow({
|
|
100693
100834
|
name: name21,
|
|
@@ -100710,6 +100851,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
100710
100851
|
sandboxImage: opts.sandboxImage ?? opts.e2bTemplate,
|
|
100711
100852
|
sandboxRemoteDir: opts.sandboxRemoteDir,
|
|
100712
100853
|
sandboxCleanup: opts.sandboxCleanup,
|
|
100854
|
+
sandboxSyncStrategy: opts.sandboxSync,
|
|
100713
100855
|
setupCommand: opts.sandboxSetupCommand,
|
|
100714
100856
|
packageSpec: opts.sandboxPackage,
|
|
100715
100857
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
@@ -100778,7 +100920,7 @@ workflowCmd.command("run <id>").description("Run a saved testing workflow").requ
|
|
|
100778
100920
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
100779
100921
|
dryRun: opts.dryRun
|
|
100780
100922
|
});
|
|
100781
|
-
if (opts.json || opts.dryRun || output.
|
|
100923
|
+
if (opts.json || opts.dryRun || output.sandboxResult) {
|
|
100782
100924
|
log(JSON.stringify(output, null, 2));
|
|
100783
100925
|
return;
|
|
100784
100926
|
}
|
|
@@ -100789,6 +100931,44 @@ workflowCmd.command("run <id>").description("Run a saved testing workflow").requ
|
|
|
100789
100931
|
process.exit(1);
|
|
100790
100932
|
}
|
|
100791
100933
|
});
|
|
100934
|
+
workflowCmd.command("fanout [ids...]").description("Run multiple saved sandbox workflows concurrently").requiredOption("-u, --url <url>", "Target URL").option("--project <id>", "Project ID").option("--tag <tag>", "Workflow scenario tag filter (repeatable)", (val, acc) => {
|
|
100935
|
+
acc.push(val);
|
|
100936
|
+
return acc;
|
|
100937
|
+
}, []).option("--all", "Include disabled workflows when selecting by project/tag", false).option("--workers <n>", "Concurrent sandboxes, 1-12 (default: 6)", "6").option("-m, --model <model>", "AI model").option("--headed", "Run headed", false).option("--parallel <n>", "Parallel browser workers inside each sandbox").option("--timeout <ms>", "Override workflow timeout").option("--dry-run", "Print resolved sandbox plans without spawning sandboxes", false).option("--json", "Output as JSON", false).action(async (ids, opts) => {
|
|
100938
|
+
try {
|
|
100939
|
+
const { runWorkflowFanout: runWorkflowFanout2 } = await Promise.resolve().then(() => (init_workflow_fanout(), exports_workflow_fanout));
|
|
100940
|
+
const result = await runWorkflowFanout2({
|
|
100941
|
+
workflowIds: ids,
|
|
100942
|
+
projectId: opts.project ? resolveProject2(opts.project) : undefined,
|
|
100943
|
+
tags: opts.tag,
|
|
100944
|
+
includeDisabled: opts.all,
|
|
100945
|
+
workers: opts.workers ? parseInt(opts.workers, 10) : undefined,
|
|
100946
|
+
url: opts.url,
|
|
100947
|
+
model: opts.model,
|
|
100948
|
+
headed: opts.headed,
|
|
100949
|
+
parallel: opts.parallel ? parseInt(opts.parallel, 10) : undefined,
|
|
100950
|
+
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
100951
|
+
dryRun: opts.dryRun
|
|
100952
|
+
});
|
|
100953
|
+
if (opts.json || opts.dryRun) {
|
|
100954
|
+
log(JSON.stringify(result, null, 2));
|
|
100955
|
+
} else {
|
|
100956
|
+
const status = result.status === "passed" ? chalk6.green("passed") : chalk6.red("failed");
|
|
100957
|
+
log(chalk6.bold(`Sandbox workflow fanout ${status}: ${result.passed}/${result.total} passed with ${result.workers} worker(s)`));
|
|
100958
|
+
for (const item of result.items) {
|
|
100959
|
+
const itemStatus = item.status === "passed" ? chalk6.green(item.status) : chalk6.red(item.status);
|
|
100960
|
+
const sandbox = item.sandboxId ? chalk6.dim(` sandbox=${item.sandboxId.slice(0, 8)}`) : "";
|
|
100961
|
+
const error40 = item.error ? chalk6.dim(` ${item.error}`) : "";
|
|
100962
|
+
log(` ${itemStatus} ${item.workflowName}${sandbox}${error40}`);
|
|
100963
|
+
}
|
|
100964
|
+
}
|
|
100965
|
+
if (result.status === "failed")
|
|
100966
|
+
process.exit(1);
|
|
100967
|
+
} catch (error40) {
|
|
100968
|
+
logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
|
|
100969
|
+
process.exit(1);
|
|
100970
|
+
}
|
|
100971
|
+
});
|
|
100792
100972
|
workflowCmd.command("agent <id>").description("Run a saved workflow as a goal loop and ask the app AI to create open-todos next actions").requiredOption("-u, --url <url>", "Target URL").option("-m, --model <model>", "AI SDK model ID for planning").option("--headed", "Run headed", false).option("--parallel <n>", "Parallel workers").option("--dry-run", "Resolve the loop without launching browsers", false).option("--json", "Output as JSON", false).action(async (id, opts) => {
|
|
100793
100973
|
try {
|
|
100794
100974
|
const { runWorkflowGoalLoop: runWorkflowGoalLoop2 } = await Promise.resolve().then(() => (init_workflow_agent(), exports_workflow_agent));
|