site-agent-pro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { renderHtmlReport } from "../reporting/html.js";
|
|
3
|
+
import { AccessibilityResultSchema, FinalReportSchema, SiteChecksSchema, TaskRunResultSchema } from "../schemas/types.js";
|
|
4
|
+
import { DashboardRunDetailSchema, DashboardRunSummarySchema, RunInputsSchema } from "../dashboard/contracts.js";
|
|
5
|
+
const TaskRunResultsSchema = z.array(TaskRunResultSchema);
|
|
6
|
+
const RawEventsSchema = z.array(z.unknown());
|
|
7
|
+
function readHost(baseUrl, fallback) {
|
|
8
|
+
if (!baseUrl) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return new URL(baseUrl).hostname.replace(/^www\./, "") || fallback;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isVisibleDashboardRun(run) {
|
|
19
|
+
return run.batchRole !== "child";
|
|
20
|
+
}
|
|
21
|
+
function collectSiteCheckWarnings(runId, siteChecks) {
|
|
22
|
+
if (!siteChecks) {
|
|
23
|
+
return [
|
|
24
|
+
`This run is missing supplemental site checks. Run \`npm run backfill:site-checks -- --run ${runId}\` or rerun the audit to verify performance, SEO, security, content, and mobile responsiveness.`
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
const blockers = Object.values(siteChecks.coverage).flatMap((coverage) => coverage.blockers);
|
|
28
|
+
if (blockers.some((blocker) => /__name is not defined/i.test(blocker))) {
|
|
29
|
+
return [
|
|
30
|
+
`This run used an older broken probe build. Run \`npm run backfill:site-checks -- --run ${runId} --force\` or rerun the audit to refresh the blocked metrics, including the mobile responsiveness check.`
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
async function findVideoArtifact(repository, runId) {
|
|
36
|
+
try {
|
|
37
|
+
const files = await repository.listArtifacts(runId);
|
|
38
|
+
const video = files.find((f) => f.endsWith(".webm") || f.endsWith(".mp4"));
|
|
39
|
+
return video ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function enrichAgentRunsWithReplay(repository, agentRuns) {
|
|
46
|
+
return Promise.all(agentRuns.map(async (agentRun) => {
|
|
47
|
+
const childRunId = agentRun.runId?.trim();
|
|
48
|
+
if (!childRunId) {
|
|
49
|
+
return agentRun;
|
|
50
|
+
}
|
|
51
|
+
const clickReplayAvailable = (await repository.readBinaryArtifact(childRunId, "click-replay.webp")) !== null;
|
|
52
|
+
const videoArtifact = await findVideoArtifact(repository, childRunId);
|
|
53
|
+
return {
|
|
54
|
+
...agentRun,
|
|
55
|
+
clickReplayAvailable,
|
|
56
|
+
clickReplayArtifact: clickReplayAvailable ? "click-replay.webp" : null,
|
|
57
|
+
...(videoArtifact ? { videoArtifact } : {})
|
|
58
|
+
};
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
export async function buildRunSummary(repository, runId) {
|
|
62
|
+
const inputs = await repository.readJsonArtifact(runId, "inputs.json", RunInputsSchema);
|
|
63
|
+
const report = await repository.readJsonArtifact(runId, "report.json", FinalReportSchema);
|
|
64
|
+
const accessibility = await repository.readJsonArtifact(runId, "accessibility.json", AccessibilityResultSchema);
|
|
65
|
+
return DashboardRunSummarySchema.parse({
|
|
66
|
+
id: runId,
|
|
67
|
+
baseUrl: inputs?.baseUrl ?? "",
|
|
68
|
+
host: readHost(inputs?.baseUrl, runId),
|
|
69
|
+
startedAt: inputs?.startedAt ?? null,
|
|
70
|
+
headed: inputs?.headed ?? false,
|
|
71
|
+
mobile: inputs?.mobile ?? false,
|
|
72
|
+
llmProvider: inputs?.llmProvider ?? null,
|
|
73
|
+
model: inputs?.model ?? null,
|
|
74
|
+
persona: inputs?.persona ?? null,
|
|
75
|
+
overallScore: report?.overall_score ?? null,
|
|
76
|
+
summary: report?.summary ?? null,
|
|
77
|
+
taskCount: report?.task_results.length ?? 0,
|
|
78
|
+
accessibilityViolationCount: accessibility?.violations.length ?? null,
|
|
79
|
+
batchRole: inputs?.batchRole ?? "single",
|
|
80
|
+
agentCount: inputs?.agentCount ?? 1,
|
|
81
|
+
completedAgentCount: inputs?.completedAgentCount ?? 0,
|
|
82
|
+
failedAgentCount: inputs?.failedAgentCount ?? 0,
|
|
83
|
+
agentLabel: inputs?.agentLabel ?? null,
|
|
84
|
+
agentProfileLabel: inputs?.agentProfileLabel ?? null
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export async function listVisibleRunSummaries(repository) {
|
|
88
|
+
const runIds = await repository.listRunIds();
|
|
89
|
+
const allRuns = await Promise.all(runIds.map((runId) => buildRunSummary(repository, runId)));
|
|
90
|
+
return allRuns.filter(isVisibleDashboardRun);
|
|
91
|
+
}
|
|
92
|
+
export async function buildRunDetail(repository, runId) {
|
|
93
|
+
const inputs = await repository.readJsonArtifact(runId, "inputs.json", RunInputsSchema);
|
|
94
|
+
const inferredClickReplayArtifact = (await repository.readBinaryArtifact(runId, "click-replay.webp")) !== null ? "click-replay.webp" : null;
|
|
95
|
+
const inferredVideoArtifact = await findVideoArtifact(repository, runId);
|
|
96
|
+
const inputsWithReplay = inputs
|
|
97
|
+
? {
|
|
98
|
+
...inputs,
|
|
99
|
+
...(inferredClickReplayArtifact && !inputs.clickReplayArtifact ? { clickReplayArtifact: inferredClickReplayArtifact } : {}),
|
|
100
|
+
...(inferredVideoArtifact ? { videoArtifact: inferredVideoArtifact } : {}),
|
|
101
|
+
agentRuns: await enrichAgentRunsWithReplay(repository, inputs.agentRuns)
|
|
102
|
+
}
|
|
103
|
+
: inputs;
|
|
104
|
+
const report = await repository.readJsonArtifact(runId, "report.json", FinalReportSchema);
|
|
105
|
+
const accessibility = await repository.readJsonArtifact(runId, "accessibility.json", AccessibilityResultSchema);
|
|
106
|
+
const siteChecks = await repository.readJsonArtifact(runId, "site-checks.json", SiteChecksSchema);
|
|
107
|
+
const taskRuns = (await repository.readJsonArtifact(runId, "task-results.json", TaskRunResultsSchema)) ?? [];
|
|
108
|
+
const rawEvents = (await repository.readJsonArtifact(runId, "raw-events.json", RawEventsSchema)) ?? [];
|
|
109
|
+
if (!inputs && !report && !accessibility && !siteChecks && taskRuns.length === 0 && rawEvents.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const warnings = [];
|
|
113
|
+
if (!inputs) {
|
|
114
|
+
warnings.push("inputs.json is missing or invalid for this run.");
|
|
115
|
+
}
|
|
116
|
+
if (!report) {
|
|
117
|
+
warnings.push("report.json is missing or invalid for this run.");
|
|
118
|
+
}
|
|
119
|
+
if (!accessibility) {
|
|
120
|
+
warnings.push("accessibility.json is missing or invalid for this run.");
|
|
121
|
+
}
|
|
122
|
+
warnings.push(...collectSiteCheckWarnings(runId, siteChecks));
|
|
123
|
+
if (taskRuns.length === 0) {
|
|
124
|
+
warnings.push("task-results.json is missing or empty for this run.");
|
|
125
|
+
}
|
|
126
|
+
const reviewedTasks = new Map((report?.task_results ?? []).map((task) => [task.name, task]));
|
|
127
|
+
const taskRunsByName = new Map(taskRuns.map((task) => [task.name, task]));
|
|
128
|
+
const taskNames = Array.from(new Set([...taskRuns.map((task) => task.name), ...(report?.task_results.map((task) => task.name) ?? [])]));
|
|
129
|
+
const tasks = taskNames.map((taskName) => {
|
|
130
|
+
const taskRun = taskRunsByName.get(taskName);
|
|
131
|
+
const reviewedTask = reviewedTasks.get(taskName);
|
|
132
|
+
return {
|
|
133
|
+
name: taskName,
|
|
134
|
+
status: reviewedTask?.status ?? taskRun?.status ?? "failed",
|
|
135
|
+
reason: reviewedTask?.reason ?? taskRun?.reason ?? "No task reasoning was captured for this task.",
|
|
136
|
+
evidence: reviewedTask?.evidence ?? [],
|
|
137
|
+
finalUrl: taskRun?.finalUrl ?? "",
|
|
138
|
+
finalTitle: taskRun?.finalTitle ?? "",
|
|
139
|
+
history: taskRun?.history ?? []
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
return DashboardRunDetailSchema.parse({
|
|
143
|
+
id: runId,
|
|
144
|
+
host: readHost(inputsWithReplay?.baseUrl, runId),
|
|
145
|
+
inputs: inputsWithReplay,
|
|
146
|
+
report,
|
|
147
|
+
accessibility,
|
|
148
|
+
siteChecks,
|
|
149
|
+
tasks,
|
|
150
|
+
rawEventCount: rawEvents.length,
|
|
151
|
+
warnings
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export async function buildStandaloneReportHtml(repository, runId) {
|
|
155
|
+
const inputs = await repository.readJsonArtifact(runId, "inputs.json", RunInputsSchema);
|
|
156
|
+
const report = await repository.readJsonArtifact(runId, "report.json", FinalReportSchema);
|
|
157
|
+
const accessibility = await repository.readJsonArtifact(runId, "accessibility.json", AccessibilityResultSchema);
|
|
158
|
+
const siteChecks = await repository.readJsonArtifact(runId, "site-checks.json", SiteChecksSchema);
|
|
159
|
+
const taskRuns = (await repository.readJsonArtifact(runId, "task-results.json", TaskRunResultsSchema)) ?? [];
|
|
160
|
+
const rawEvents = (await repository.readJsonArtifact(runId, "raw-events.json", RawEventsSchema)) ?? [];
|
|
161
|
+
if (!report) {
|
|
162
|
+
return repository.readTextArtifact(runId, "report.html");
|
|
163
|
+
}
|
|
164
|
+
return renderHtmlReport({
|
|
165
|
+
website: inputs?.baseUrl ?? runId,
|
|
166
|
+
persona: inputs?.persona ?? "first-time visitor",
|
|
167
|
+
acceptedTasks: inputs?.customTasks ?? [],
|
|
168
|
+
instructionText: inputs?.instructionText,
|
|
169
|
+
report,
|
|
170
|
+
taskResults: taskRuns,
|
|
171
|
+
accessibility: accessibility ?? undefined,
|
|
172
|
+
siteChecks: siteChecks ?? undefined,
|
|
173
|
+
siteBrief: inputs?.siteBrief,
|
|
174
|
+
rawEvents,
|
|
175
|
+
runId,
|
|
176
|
+
startedAt: inputs?.startedAt,
|
|
177
|
+
mobile: inputs?.mobile,
|
|
178
|
+
timeZone: inputs?.synchronizedTimezone ?? inputs?.browserTimezone ?? inputs?.deviceTimezone
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export async function loadDashboardData(repository, selectedRunId) {
|
|
182
|
+
const runs = await listVisibleRunSummaries(repository);
|
|
183
|
+
let resolvedRunId = selectedRunId;
|
|
184
|
+
let detail = selectedRunId ? await buildRunDetail(repository, selectedRunId) : null;
|
|
185
|
+
if (!detail) {
|
|
186
|
+
resolvedRunId = runs.find((run) => run.id === selectedRunId)?.id ?? runs[0]?.id ?? null;
|
|
187
|
+
detail = resolvedRunId ? await buildRunDetail(repository, resolvedRunId) : null;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
runs,
|
|
191
|
+
detail,
|
|
192
|
+
selectedRunId: resolvedRunId
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const SAFE_RUN_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
3
|
+
const IMAGE_ARTIFACT_PATTERN = /^[A-Za-z0-9._-]+\.(?:png|jpg|jpeg|webp)$/i;
|
|
4
|
+
const VIDEO_ARTIFACT_PATTERN = /^[A-Za-z0-9._-]+\.(?:webm|mp4)$/i;
|
|
5
|
+
const STATIC_REPORT_ARTIFACTS = new Set(["report.html", "report.json", "report.md"]);
|
|
6
|
+
export function isSafeRunFileName(value) {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
return trimmed.length > 0 && path.basename(trimmed) === trimmed && SAFE_RUN_NAME_PATTERN.test(trimmed);
|
|
9
|
+
}
|
|
10
|
+
export function isImageArtifact(fileName) {
|
|
11
|
+
return IMAGE_ARTIFACT_PATTERN.test(fileName.trim());
|
|
12
|
+
}
|
|
13
|
+
export function isVideoArtifact(fileName) {
|
|
14
|
+
return VIDEO_ARTIFACT_PATTERN.test(fileName.trim());
|
|
15
|
+
}
|
|
16
|
+
export function isStaticReportArtifact(fileName) {
|
|
17
|
+
return STATIC_REPORT_ARTIFACTS.has(fileName.trim());
|
|
18
|
+
}
|
|
19
|
+
export function isAllowedDashboardArtifact(fileName) {
|
|
20
|
+
return isStaticReportArtifact(fileName) || isImageArtifact(fileName) || isVideoArtifact(fileName);
|
|
21
|
+
}
|
|
22
|
+
export function artifactContentType(fileName) {
|
|
23
|
+
if (fileName.endsWith(".html")) {
|
|
24
|
+
return "text/html; charset=utf-8";
|
|
25
|
+
}
|
|
26
|
+
if (fileName.endsWith(".json")) {
|
|
27
|
+
return "application/json; charset=utf-8";
|
|
28
|
+
}
|
|
29
|
+
if (fileName.endsWith(".md")) {
|
|
30
|
+
return "text/markdown; charset=utf-8";
|
|
31
|
+
}
|
|
32
|
+
if (fileName.endsWith(".png")) {
|
|
33
|
+
return "image/png";
|
|
34
|
+
}
|
|
35
|
+
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
|
|
36
|
+
return "image/jpeg";
|
|
37
|
+
}
|
|
38
|
+
if (fileName.endsWith(".webp")) {
|
|
39
|
+
return "image/webp";
|
|
40
|
+
}
|
|
41
|
+
if (fileName.endsWith(".webm")) {
|
|
42
|
+
return "video/webm";
|
|
43
|
+
}
|
|
44
|
+
if (fileName.endsWith(".mp4")) {
|
|
45
|
+
return "video/mp4";
|
|
46
|
+
}
|
|
47
|
+
return "application/octet-stream";
|
|
48
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readUtf8, resolveRunsDir } from "../utils/files.js";
|
|
4
|
+
import { isSafeRunFileName } from "./runArtifacts.js";
|
|
5
|
+
function normalizeRunId(runId) {
|
|
6
|
+
const trimmed = runId.trim();
|
|
7
|
+
return isSafeRunFileName(trimmed) ? trimmed : null;
|
|
8
|
+
}
|
|
9
|
+
function resolveLocalRunDir(runId) {
|
|
10
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
11
|
+
if (!normalizedRunId) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const baseDir = resolveRunsDir();
|
|
15
|
+
const runDir = path.resolve(baseDir, normalizedRunId);
|
|
16
|
+
if (!runDir.startsWith(`${baseDir}${path.sep}`)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!fs.existsSync(runDir) || !fs.statSync(runDir).isDirectory()) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return runDir;
|
|
23
|
+
}
|
|
24
|
+
function resolveLocalArtifactPath(runId, fileName) {
|
|
25
|
+
const runDir = resolveLocalRunDir(runId);
|
|
26
|
+
const normalizedFileName = isSafeRunFileName(fileName) ? fileName.trim() : null;
|
|
27
|
+
if (!runDir || !normalizedFileName) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const artifactPath = path.resolve(runDir, normalizedFileName);
|
|
31
|
+
if (!artifactPath.startsWith(`${runDir}${path.sep}`)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!fs.existsSync(artifactPath) || !fs.statSync(artifactPath).isFile()) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return artifactPath;
|
|
38
|
+
}
|
|
39
|
+
export function createRunRepository(adapter) {
|
|
40
|
+
return {
|
|
41
|
+
listRunIds: adapter.listRunIds,
|
|
42
|
+
listArtifacts: adapter.listArtifacts ??
|
|
43
|
+
(async (runId) => {
|
|
44
|
+
const runDir = resolveLocalRunDir(runId);
|
|
45
|
+
if (!runDir)
|
|
46
|
+
return [];
|
|
47
|
+
return fs.readdirSync(runDir).filter((f) => isSafeRunFileName(f));
|
|
48
|
+
}),
|
|
49
|
+
hasRun: adapter.hasRun ??
|
|
50
|
+
(async (runId) => {
|
|
51
|
+
const runIds = await adapter.listRunIds();
|
|
52
|
+
return runIds.includes(runId);
|
|
53
|
+
}),
|
|
54
|
+
readTextArtifact: adapter.readTextArtifact,
|
|
55
|
+
readBinaryArtifact: adapter.readBinaryArtifact,
|
|
56
|
+
readJsonArtifact: adapter.readJsonArtifact,
|
|
57
|
+
resolveArtifactPath: adapter.resolveArtifactPath ??
|
|
58
|
+
(async (runId, fileName) => {
|
|
59
|
+
return resolveLocalArtifactPath(runId, fileName);
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function createLocalRunRepository() {
|
|
64
|
+
return createRunRepository({
|
|
65
|
+
listRunIds: async () => {
|
|
66
|
+
const runsDir = resolveRunsDir();
|
|
67
|
+
if (!fs.existsSync(runsDir)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return fs
|
|
71
|
+
.readdirSync(runsDir, { withFileTypes: true })
|
|
72
|
+
.filter((entry) => entry.isDirectory())
|
|
73
|
+
.map((entry) => entry.name)
|
|
74
|
+
.sort((left, right) => right.localeCompare(left));
|
|
75
|
+
},
|
|
76
|
+
hasRun: async (runId) => resolveLocalRunDir(runId) !== null,
|
|
77
|
+
readTextArtifact: async (runId, fileName) => {
|
|
78
|
+
const artifactPath = resolveLocalArtifactPath(runId, fileName);
|
|
79
|
+
return artifactPath ? readUtf8(artifactPath) : null;
|
|
80
|
+
},
|
|
81
|
+
readBinaryArtifact: async (runId, fileName) => {
|
|
82
|
+
const artifactPath = resolveLocalArtifactPath(runId, fileName);
|
|
83
|
+
return artifactPath ? fs.readFileSync(artifactPath) : null;
|
|
84
|
+
},
|
|
85
|
+
readJsonArtifact: async (runId, fileName, schema) => {
|
|
86
|
+
const artifactPath = resolveLocalArtifactPath(runId, fileName);
|
|
87
|
+
if (!artifactPath) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return schema.parse(JSON.parse(readUtf8(artifactPath)));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { renderHtmlReport } from "../reporting/html.js";
|
|
7
|
+
import { renderMarkdownReport } from "../reporting/markdown.js";
|
|
8
|
+
import { AccessibilityResultSchema, FinalReportSchema, SiteChecksSchema, TaskRunResultSchema } from "../schemas/types.js";
|
|
9
|
+
import { RunInputsSchema } from "../dashboard/contracts.js";
|
|
10
|
+
import { runSiteChecks } from "../core/siteChecks.js";
|
|
11
|
+
import { readUtf8, resolveRunsDir, writeJson, writeText } from "../utils/files.js";
|
|
12
|
+
const TaskRunResultsSchema = TaskRunResultSchema.array();
|
|
13
|
+
const RawEventsSchema = z.array(z.unknown());
|
|
14
|
+
function loadJson(filePath, schema) {
|
|
15
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return schema.parse(JSON.parse(readUtf8(filePath)));
|
|
19
|
+
}
|
|
20
|
+
function needsProbeRefresh(siteChecks) {
|
|
21
|
+
if (!siteChecks) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return Object.values(siteChecks.coverage).some((coverage) => coverage.blockers.some((blocker) => /__name is not defined|scrollWidth|Timeout \d+ms exceeded|ERR_CONNECTION_CLOSED|ERR_SOCKET_NOT_CONNECTED/i.test(blocker)));
|
|
25
|
+
}
|
|
26
|
+
async function backfillRun(runId, force, budgetMs) {
|
|
27
|
+
const runDir = path.join(resolveRunsDir(), runId);
|
|
28
|
+
if (!fs.existsSync(runDir) || !fs.statSync(runDir).isDirectory()) {
|
|
29
|
+
return { runId, status: "failed", note: "Run directory was not found." };
|
|
30
|
+
}
|
|
31
|
+
const siteChecksPath = path.join(runDir, "site-checks.json");
|
|
32
|
+
const existingSiteChecks = fs.existsSync(siteChecksPath) ? loadJson(siteChecksPath, SiteChecksSchema) : null;
|
|
33
|
+
if (!force && existingSiteChecks && !needsProbeRefresh(existingSiteChecks)) {
|
|
34
|
+
return { runId, status: "skipped", note: "site-checks.json already exists." };
|
|
35
|
+
}
|
|
36
|
+
if (!force && existingSiteChecks && needsProbeRefresh(existingSiteChecks)) {
|
|
37
|
+
process.stdout.write(`[refresh] ${runId}: existing site checks were created by an older broken probe build.\n`);
|
|
38
|
+
}
|
|
39
|
+
if (!force && fs.existsSync(siteChecksPath) && !existingSiteChecks) {
|
|
40
|
+
process.stdout.write(`[refresh] ${runId}: existing site-checks.json was invalid and will be regenerated.\n`);
|
|
41
|
+
}
|
|
42
|
+
const inputs = loadJson(path.join(runDir, "inputs.json"), RunInputsSchema);
|
|
43
|
+
const report = loadJson(path.join(runDir, "report.json"), FinalReportSchema);
|
|
44
|
+
const accessibility = loadJson(path.join(runDir, "accessibility.json"), AccessibilityResultSchema);
|
|
45
|
+
const taskResults = loadJson(path.join(runDir, "task-results.json"), TaskRunResultsSchema) ?? [];
|
|
46
|
+
const rawEvents = loadJson(path.join(runDir, "raw-events.json"), RawEventsSchema) ?? [];
|
|
47
|
+
if (!inputs || !report) {
|
|
48
|
+
return { runId, status: "failed", note: "inputs.json or report.json is missing." };
|
|
49
|
+
}
|
|
50
|
+
const browser = await chromium.launch({ headless: true });
|
|
51
|
+
try {
|
|
52
|
+
const siteChecks = await runSiteChecks({
|
|
53
|
+
browser,
|
|
54
|
+
baseUrl: inputs.baseUrl,
|
|
55
|
+
ignoreHttpsErrors: Boolean(inputs.ignoreHttpsErrors),
|
|
56
|
+
browserTimezone: inputs.synchronizedTimezone ?? inputs.browserTimezone ?? inputs.deviceTimezone ?? "UTC",
|
|
57
|
+
storageState: undefined,
|
|
58
|
+
rawEvents: rawEvents ?? [],
|
|
59
|
+
taskResults,
|
|
60
|
+
budgetMs
|
|
61
|
+
});
|
|
62
|
+
writeJson(siteChecksPath, siteChecks);
|
|
63
|
+
writeText(path.join(runDir, "report.html"), renderHtmlReport({
|
|
64
|
+
website: inputs.baseUrl,
|
|
65
|
+
persona: inputs.persona ?? "first-time visitor",
|
|
66
|
+
report,
|
|
67
|
+
taskResults,
|
|
68
|
+
accessibility: accessibility ?? undefined,
|
|
69
|
+
siteChecks,
|
|
70
|
+
rawEvents: rawEvents ?? [],
|
|
71
|
+
runId,
|
|
72
|
+
startedAt: inputs.startedAt,
|
|
73
|
+
mobile: inputs.mobile,
|
|
74
|
+
timeZone: inputs.synchronizedTimezone ?? inputs.browserTimezone ?? inputs.deviceTimezone
|
|
75
|
+
}));
|
|
76
|
+
writeText(path.join(runDir, "report.md"), renderMarkdownReport({
|
|
77
|
+
website: inputs.baseUrl,
|
|
78
|
+
persona: inputs.persona ?? "first-time visitor",
|
|
79
|
+
report,
|
|
80
|
+
taskResults,
|
|
81
|
+
accessibility: accessibility ?? undefined,
|
|
82
|
+
siteChecks,
|
|
83
|
+
rawEvents: rawEvents ?? [],
|
|
84
|
+
startedAt: inputs.startedAt,
|
|
85
|
+
mobile: inputs.mobile,
|
|
86
|
+
timeZone: inputs.synchronizedTimezone ?? inputs.browserTimezone ?? inputs.deviceTimezone
|
|
87
|
+
}));
|
|
88
|
+
return { runId, status: "updated", note: "site-checks.json and rendered reports were refreshed." };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
runId,
|
|
93
|
+
status: "failed",
|
|
94
|
+
note: error instanceof Error ? error.message : "Unknown backfill failure"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await browser.close().catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function main() {
|
|
102
|
+
const program = new Command();
|
|
103
|
+
program
|
|
104
|
+
.option("--run <runId>", "Backfill a single run by run ID")
|
|
105
|
+
.option("--all", "Backfill every run under the runs directory")
|
|
106
|
+
.option("--visible-only", "When used with --all, skip child agent runs that are hidden from the dashboard", false)
|
|
107
|
+
.option("--budget-ms <ms>", "Override the supplemental site-check budget used during backfill", "45000")
|
|
108
|
+
.option("--force", "Recompute site checks even if they already exist", false);
|
|
109
|
+
program.parse(process.argv);
|
|
110
|
+
const options = program.opts();
|
|
111
|
+
const parsedBudgetMs = Number.parseInt(options.budgetMs ?? "45000", 10);
|
|
112
|
+
const budgetMs = Number.isFinite(parsedBudgetMs) ? Math.max(12000, parsedBudgetMs) : 45000;
|
|
113
|
+
const runIds = options.all
|
|
114
|
+
? fs
|
|
115
|
+
.readdirSync(resolveRunsDir(), { withFileTypes: true })
|
|
116
|
+
.filter((entry) => entry.isDirectory())
|
|
117
|
+
.filter((entry) => {
|
|
118
|
+
if (!options.visibleOnly) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const inputs = loadJson(path.join(resolveRunsDir(), entry.name, "inputs.json"), RunInputsSchema);
|
|
122
|
+
if (!inputs) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return (inputs.batchRole ?? "single") !== "child";
|
|
126
|
+
})
|
|
127
|
+
.map((entry) => entry.name)
|
|
128
|
+
.sort((left, right) => right.localeCompare(left))
|
|
129
|
+
: options.run
|
|
130
|
+
? [options.run]
|
|
131
|
+
: [];
|
|
132
|
+
if (runIds.length === 0) {
|
|
133
|
+
throw new Error("Provide either --run <runId> or --all.");
|
|
134
|
+
}
|
|
135
|
+
for (const runId of runIds) {
|
|
136
|
+
const result = await backfillRun(runId, Boolean(options.force), budgetMs);
|
|
137
|
+
process.stdout.write(`[${result.status}] ${result.runId}: ${result.note}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
main().catch((error) => {
|
|
141
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
});
|