preflite 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +144 -0
  3. package/dist/adapter-spi/artifact/index.js +1 -0
  4. package/dist/adapter-spi/command/index.js +1 -0
  5. package/dist/adapter-spi/install/index.js +1 -0
  6. package/dist/adapter-spi/lifecycle/index.js +1 -0
  7. package/dist/adapter-spi/resource/index.js +1 -0
  8. package/dist/adapter-spi/snapshot/index.js +1 -0
  9. package/dist/application/agent/AgentCommandPollLoop.js +48 -0
  10. package/dist/application/agent/AgentRuntimeService.js +27 -0
  11. package/dist/application/app-package/AppPackageApplicationService.js +97 -0
  12. package/dist/application/artifact/ArtifactApplicationService.js +13 -0
  13. package/dist/application/debug/DebugApplicationService.js +117 -0
  14. package/dist/application/health/HealthMetricsService.js +47 -0
  15. package/dist/application/lease/LeaseApplicationService.js +79 -0
  16. package/dist/application/query/ObservationQueryService.js +48 -0
  17. package/dist/application/reporter/ReporterApplicationService.js +41 -0
  18. package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
  19. package/dist/application/resource/ResourceRegistryService.js +113 -0
  20. package/dist/application/session/SessionApplicationService.js +39 -0
  21. package/dist/application/task/TaskApplicationService.js +378 -0
  22. package/dist/client/agentAppPackageClient.js +91 -0
  23. package/dist/domain/agent/AgentNode.js +12 -0
  24. package/dist/domain/agent/AgentRuntime.js +6 -0
  25. package/dist/domain/artifact/ArtifactPipeline.js +6 -0
  26. package/dist/domain/artifact/ArtifactRef.js +12 -0
  27. package/dist/domain/event/AgentEvent.js +10 -0
  28. package/dist/domain/event/Reporter.js +6 -0
  29. package/dist/domain/health/HealthMetrics.js +8 -0
  30. package/dist/domain/lease/Lease.js +28 -0
  31. package/dist/domain/lease/LeaseManager.js +6 -0
  32. package/dist/domain/repositories/index.js +1 -0
  33. package/dist/domain/resource/DeviceDetails.js +23 -0
  34. package/dist/domain/resource/DeviceResource.js +16 -0
  35. package/dist/domain/resource/ResourceRegistry.js +6 -0
  36. package/dist/domain/runtime/interfaces.js +1 -0
  37. package/dist/domain/session/BaseSession.js +16 -0
  38. package/dist/domain/session/DebugSession.js +3 -0
  39. package/dist/domain/session/ExecutionSession.js +3 -0
  40. package/dist/domain/session/SessionManager.js +8 -0
  41. package/dist/domain/task/TaskRecord.js +14 -0
  42. package/dist/domain/task/TaskSpec.js +12 -0
  43. package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
  44. package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
  45. package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
  46. package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
  47. package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
  48. package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
  49. package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
  50. package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
  51. package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
  52. package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
  53. package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
  54. package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
  55. package/dist/infrastructure/config/agentConfigFile.js +96 -0
  56. package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
  57. package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
  58. package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
  59. package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
  60. package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
  61. package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
  62. package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
  63. package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
  64. package/dist/infrastructure/midscene/videoRecorder.js +365 -0
  65. package/dist/infrastructure/midscene/zipReportDir.js +36 -0
  66. package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
  67. package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
  68. package/dist/infrastructure/system/CommandRunner.js +128 -0
  69. package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
  70. package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
  71. package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
  72. package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
  73. package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
  74. package/dist/infrastructure/transport/midscenePaths.js +28 -0
  75. package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
  76. package/dist/infrastructure/transport/ws/WsClient.js +182 -0
  77. package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
  78. package/dist/interfaces/http/HttpServer.js +227 -0
  79. package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
  80. package/dist/main.js +368 -0
  81. package/dist/mcp/agentHttpClient.js +82 -0
  82. package/dist/mcp/agentRuntime.js +184 -0
  83. package/dist/mcp/cli.js +36 -0
  84. package/dist/mcp/doctor.js +124 -0
  85. package/dist/mcp/evidence.js +57 -0
  86. package/dist/mcp/exploration/index.js +129 -0
  87. package/dist/mcp/exploration/sessionManager.js +122 -0
  88. package/dist/mcp/exploration/tools-atomic.js +34 -0
  89. package/dist/mcp/exploration/tools-intelligent.js +33 -0
  90. package/dist/mcp/exploration/tools-session.js +276 -0
  91. package/dist/mcp/exploration/types.js +1 -0
  92. package/dist/mcp/flowStepEvents.js +114 -0
  93. package/dist/mcp/liveViewer.js +156 -0
  94. package/dist/mcp/reportReader.js +157 -0
  95. package/dist/mcp/runManager.js +161 -0
  96. package/dist/mcp/runSummary.js +72 -0
  97. package/dist/mcp/runtimeInstall.js +44 -0
  98. package/dist/mcp/server.js +260 -0
  99. package/dist/mcp/setup.js +185 -0
  100. package/dist/mcp/types.js +1 -0
  101. package/dist/mcp/userConfig.js +45 -0
  102. package/dist/mcp/visual-flow/codegen.js +576 -0
  103. package/dist/mcp/visual-flow/index.js +14 -0
  104. package/dist/mcp/visual-flow/types.js +5 -0
  105. package/dist/mcp/visual-flow/validate.js +617 -0
  106. package/dist/protocol-contracts/commands/envelope.js +24 -0
  107. package/dist/protocol-contracts/commands/index.js +1 -0
  108. package/dist/protocol-contracts/dto/index.js +1 -0
  109. package/dist/protocol-contracts/events/index.js +1 -0
  110. package/dist/protocol-contracts/queries/index.js +1 -0
  111. package/dist/shared-kernel/enums/index.js +70 -0
  112. package/dist/shared-kernel/errors/index.js +21 -0
  113. package/dist/shared-kernel/ids/index.js +18 -0
  114. package/dist/shared-kernel/time/index.js +3 -0
  115. package/dist/shared-kernel/value-objects/index.js +1 -0
  116. package/dist/utils/appPackageLocalPath.js +75 -0
  117. package/dist/utils/deviceResourceRouting.js +24 -0
  118. package/dist/utils/harmonyAgentDebugDevice.js +60 -0
  119. package/dist/utils/harmonyHdcDeviceId.js +134 -0
  120. package/dist/utils/iosAgentDebugDevice.js +72 -0
  121. package/dist/utils/iosMjpegCapture.js +90 -0
  122. package/dist/utils/liveDebugForegroundParse.js +71 -0
  123. package/dist/utils/midscene-device-session.js +353 -0
  124. package/dist/utils/midscene-task-cache-env.js +15 -0
  125. package/dist/utils/midsceneReportConstants.js +49 -0
  126. package/dist/utils/seedMidsceneTaskCache.js +61 -0
  127. package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
  128. package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
  129. package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
  130. package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
  131. package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
  132. package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
  133. package/dist/utils/task-runners/taskAppPackage.js +20 -0
  134. package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
  135. package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
  136. package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
  137. package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
  138. package/package.json +46 -0
@@ -0,0 +1,157 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ // ── Parser ──────────────────────────────────────────────────────────
5
+ /**
6
+ * Read a Midscene report directory (containing N.execution.json files)
7
+ * and extract the full operation path.
8
+ */
9
+ export async function readReport(reportDir) {
10
+ if (!existsSync(reportDir)) {
11
+ return { success: false, reportDir, stepCount: 0, steps: [], error: `Report directory not found: ${reportDir}` };
12
+ }
13
+ const allFiles = await readdir(reportDir);
14
+ const execFiles = allFiles
15
+ .filter((f) => /^\d+\.execution\.json$/.test(f))
16
+ .sort((a, b) => {
17
+ const na = parseInt(a.match(/^(\d+)/)[1], 10);
18
+ const nb = parseInt(b.match(/^(\d+)/)[1], 10);
19
+ return na - nb;
20
+ });
21
+ if (execFiles.length === 0) {
22
+ return { success: false, reportDir, stepCount: 0, steps: [], error: `No *.execution.json files found in: ${reportDir}` };
23
+ }
24
+ const steps = [];
25
+ for (const file of execFiles) {
26
+ const content = await readFile(join(reportDir, file), "utf8");
27
+ const step = parseExecutionFile(content, file);
28
+ if (step)
29
+ steps.push(step);
30
+ }
31
+ return { success: true, reportDir, stepCount: steps.length, steps };
32
+ }
33
+ function parseExecutionFile(content, filename) {
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(content);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ const exec = parsed.executions?.[0];
42
+ if (!exec)
43
+ return null;
44
+ // Extract name like "Terminate", "Launch", "Act - xxx", "Query - xxx", "Number - xxx", etc.
45
+ const rawName = exec.name ?? filename.replace(/\.execution\.json$/, "");
46
+ const tasks = exec.tasks ?? [];
47
+ const description = exec.description;
48
+ // Determine overall status
49
+ const hasFailedTask = tasks.some((t) => t.status === "failed");
50
+ const status = hasFailedTask ? "failed" : "finished";
51
+ // Extract AI thought from the Planning task
52
+ const planTask = tasks.find((t) => t.subType === "Plan");
53
+ const aiThought = planTask?.output?.thought ?? planTask?.thought;
54
+ // Extract all actions
55
+ const actions = [];
56
+ for (const task of tasks) {
57
+ if (task.type === "Action Space" && task.subType) {
58
+ const actionType = task.subType; // "Tap", "LongPress", "Terminate", "Launch"
59
+ const locate = task.param?.locate;
60
+ const action = { type: actionType };
61
+ if (locate?.description)
62
+ action.target = locate.description;
63
+ if (locate?.center)
64
+ action.coordinate = [locate.center[0], locate.center[1]];
65
+ if (locate?.rect) {
66
+ action.bbox = [locate.rect.left, locate.rect.top, locate.rect.width, locate.rect.height];
67
+ }
68
+ action.result = task.status === "finished" ? "success" : "failed";
69
+ actions.push(action);
70
+ }
71
+ // Also read actions from the Planning output
72
+ if (task.output?.actions) {
73
+ for (const act of task.output.actions) {
74
+ const existing = actions.find((a) => a.type === act.type && a.target === act.param?.locate?.description);
75
+ if (!existing && act.type) {
76
+ actions.push({
77
+ type: act.type,
78
+ target: act.param?.locate?.description,
79
+ coordinate: act.param?.locate?.center,
80
+ bbox: act.param?.locate?.rect
81
+ ? [act.param.locate.rect.left, act.param.locate.rect.top, act.param.locate.rect.width, act.param.locate.rect.height]
82
+ : undefined,
83
+ result: "planned",
84
+ });
85
+ }
86
+ }
87
+ }
88
+ // Also extract locate elements for LongPress
89
+ if (task.subType === "Locate" && task.output?.element) {
90
+ // Already captured by the Plan output actions
91
+ }
92
+ }
93
+ // Extract data from Insight tasks (Query / Number / Boolean / Assert)
94
+ let extractedData = undefined;
95
+ let error = undefined;
96
+ for (const task of tasks) {
97
+ if (task.type === "Insight") {
98
+ if (task.subType === "Query" || task.subType === "Number" || task.subType === "Boolean") {
99
+ extractedData = task.output;
100
+ }
101
+ if (task.subType === "Assert") {
102
+ if (task.status === "failed") {
103
+ error = task.errorMessage ?? "Assertion failed";
104
+ }
105
+ extractedData = task.output;
106
+ }
107
+ }
108
+ if (task.status === "failed" && task.errorMessage) {
109
+ error = task.errorMessage;
110
+ }
111
+ }
112
+ // Build summary from the log/output
113
+ let summary;
114
+ if (description) {
115
+ summary = description;
116
+ }
117
+ else if (rawName.startsWith("Terminate")) {
118
+ summary = `关闭应用 ${tasks.find((t) => t.subType === "Terminate")?.param ?? ""}`;
119
+ }
120
+ else if (rawName.startsWith("Launch")) {
121
+ summary = `启动应用 ${tasks.find((t) => t.subType === "Launch")?.param ?? ""}`;
122
+ }
123
+ else if (rawName.startsWith("Act -")) {
124
+ summary = rawName.slice(5).trim();
125
+ const actionLog = planTask?.output?.log;
126
+ if (actionLog)
127
+ summary = actionLog;
128
+ if (status === "failed")
129
+ summary += " (失败)";
130
+ }
131
+ else if (rawName.startsWith("Query -")) {
132
+ summary = `读取数据: ${JSON.stringify(extractedData ?? "")}`;
133
+ }
134
+ else if (rawName.startsWith("Number -")) {
135
+ summary = `计数结果: ${JSON.stringify(extractedData ?? "")}`;
136
+ }
137
+ else if (rawName.startsWith("Boolean -")) {
138
+ summary = `条件判断: ${JSON.stringify(extractedData ?? "")}`;
139
+ }
140
+ else if (rawName.startsWith("Assert -")) {
141
+ const assertResult = status === "finished" ? "通过" : "失败";
142
+ summary = `断言 ${assertResult}: ${rawName.slice(7).trim()}`;
143
+ }
144
+ else if (rawName.startsWith("Log -")) {
145
+ summary = `记录日志: ${description ?? ""}`;
146
+ }
147
+ else {
148
+ summary = rawName;
149
+ }
150
+ // If there's an error, append it to summary
151
+ if (error) {
152
+ summary += ` | 错误: ${error.slice(0, 200)}`;
153
+ }
154
+ // Derive step index from filename
155
+ const stepIndex = parseInt(filename.match(/^(\d+)/)?.[1] ?? "0", 10);
156
+ return { stepIndex, name: rawName, status, summary, aiThought, actions, extractedData, error };
157
+ }
@@ -0,0 +1,161 @@
1
+ import { mkdir, appendFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ import { buildFlowStepView, extractFlowStepEventsFromRun } from "./flowStepEvents.js";
6
+ import { summarizeRun } from "./runSummary.js";
7
+ /**
8
+ * MCP transport has a 60s hard timeout. watchRun blocking must leave
9
+ * enough headroom so the response can be delivered before the transport
10
+ * closes and kills the MCP server process. 55s is the safe ceiling.
11
+ */
12
+ const MAX_BLOCK_MS = 45_000;
13
+ const TERMINAL = new Set(["SUCCESS", "FAILED", "CANCELLED"]);
14
+ const PREFLIGHT_HOME = process.env.PREFLIGHT_HOME?.trim() || `${homedir()}/.preflight`;
15
+ function runLogPath(runId) {
16
+ return join(PREFLIGHT_HOME, "runs", runId, "run.log");
17
+ }
18
+ async function logRun(runId, event, detail) {
19
+ try {
20
+ const dir = join(PREFLIGHT_HOME, "runs", runId);
21
+ await mkdir(dir, { recursive: true });
22
+ const ts = new Date().toISOString();
23
+ await appendFile(join(dir, "run.log"), `${ts} [${event}] ${detail}\n`, "utf8");
24
+ }
25
+ catch {
26
+ // 日志写失败不影响主流程
27
+ }
28
+ }
29
+ /** 将 RunState 完整快照写入 state.json,重启后仍可查看 */
30
+ async function snapshotRun(run) {
31
+ try {
32
+ const dir = join(PREFLIGHT_HOME, "runs", run.runId);
33
+ await writeFile(join(dir, "state.json"), JSON.stringify(run, null, 2), "utf8");
34
+ }
35
+ catch {
36
+ // 快照写失败不影响主流程
37
+ }
38
+ }
39
+ export class RunManager {
40
+ client;
41
+ liveBaseUrl;
42
+ runs = new Map();
43
+ constructor(client, liveBaseUrl) {
44
+ this.client = client;
45
+ this.liveBaseUrl = liveBaseUrl;
46
+ }
47
+ setLiveBaseUrl(liveBaseUrl) {
48
+ this.liveBaseUrl = liveBaseUrl;
49
+ }
50
+ async startRun(input) {
51
+ const runId = timestampId();
52
+ const taskId = `mcp-${runId}`;
53
+ const now = new Date().toISOString();
54
+ const liveUrl = `${this.liveBaseUrl.replace(/\/$/, "")}/runs/${encodeURIComponent(runId)}/live`;
55
+ const run = {
56
+ runId,
57
+ taskId,
58
+ platform: input.platform,
59
+ resourceId: input.resourceId,
60
+ appRef: input.appRef,
61
+ testIntent: input.testIntent,
62
+ script: input.script,
63
+ visualFlow: input.visualFlow,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ liveUrl,
67
+ events: [],
68
+ artifacts: [],
69
+ };
70
+ this.runs.set(runId, run);
71
+ logRun(runId, "START", `platform=${input.platform}, testIntent=${input.testIntent ?? ""}, ${input.resourceId ? `resourceId=${input.resourceId}` : ""}`);
72
+ await this.client.createTask({
73
+ taskId,
74
+ requiredPlatform: input.platform,
75
+ script: input.script,
76
+ scriptKind: input.scriptKind,
77
+ resourceId: input.resourceId,
78
+ runtimeEnv: input.runtimeEnv,
79
+ });
80
+ await this.refreshRun(runId);
81
+ snapshotRun(this.mustGet(runId));
82
+ return summarizeRun(this.mustGet(runId));
83
+ }
84
+ async cancelRun(runId, source, detail) {
85
+ const run = this.mustGet(runId);
86
+ const now = new Date().toISOString();
87
+ run.termination = { source, detail, timestamp: now };
88
+ run.updatedAt = now;
89
+ logRun(runId, "CANCEL", `source=${source}, detail=${detail}`);
90
+ snapshotRun(run);
91
+ return summarizeRun(run);
92
+ }
93
+ async watchRun(runId, minIntervalMs) {
94
+ await this.refreshRun(runId);
95
+ const summary = summarizeRun(this.mustGet(runId));
96
+ if (TERMINAL.has(summary.status)) {
97
+ logRun(runId, "END", `status=${summary.status}, failure=${summary.failureAnalysis.category}`);
98
+ return summary;
99
+ }
100
+ if (!minIntervalMs || minIntervalMs <= 0) {
101
+ return summary;
102
+ }
103
+ // 安全上限:MCP transport 有 60s 硬超时,阻塞必须留余量
104
+ const safeInterval = Math.min(minIntervalMs, MAX_BLOCK_MS);
105
+ const deadline = Date.now() + safeInterval;
106
+ while (Date.now() < deadline) {
107
+ await sleep(Math.min(2000, deadline - Date.now()));
108
+ await this.refreshRun(runId);
109
+ const latest = summarizeRun(this.mustGet(runId));
110
+ if (TERMINAL.has(latest.status)) {
111
+ logRun(runId, "END", `status=${latest.status}, failure=${latest.failureAnalysis.category}`);
112
+ return latest;
113
+ }
114
+ }
115
+ await this.refreshRun(runId);
116
+ return summarizeRun(this.mustGet(runId));
117
+ }
118
+ getRun(runId) {
119
+ return this.runs.get(runId);
120
+ }
121
+ listRuns() {
122
+ return [...this.runs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
123
+ }
124
+ async waitForRun(runId, timeoutMs, pollIntervalMs) {
125
+ const deadline = Date.now() + timeoutMs;
126
+ while (Date.now() < deadline) {
127
+ const summary = await this.watchRun(runId);
128
+ if (TERMINAL.has(summary.status))
129
+ return summary;
130
+ await sleep(pollIntervalMs);
131
+ }
132
+ return this.watchRun(runId);
133
+ }
134
+ async refreshRun(runId) {
135
+ const run = this.mustGet(runId);
136
+ const [task, events, artifacts] = await Promise.all([
137
+ this.client.getTask(run.taskId),
138
+ this.client.listEvents(run.taskId).catch(() => run.events),
139
+ this.client.listArtifacts(run.taskId).catch(() => run.artifacts),
140
+ ]);
141
+ run.task = task ?? run.task;
142
+ run.events = events;
143
+ run.artifacts = artifacts;
144
+ run.flowStepView = buildFlowStepView(run.visualFlow, extractFlowStepEventsFromRun(run));
145
+ run.updatedAt = new Date().toISOString();
146
+ snapshotRun(run);
147
+ }
148
+ mustGet(runId) {
149
+ const run = this.runs.get(runId);
150
+ if (!run)
151
+ throw new Error(`Unknown runId: ${runId}`);
152
+ return run;
153
+ }
154
+ }
155
+ function timestampId() {
156
+ const stamp = new Date().toISOString().replaceAll(/[-:]/g, "").replace(/\..+$/, "");
157
+ return `${stamp}-${randomUUID().slice(0, 8)}`;
158
+ }
159
+ function sleep(ms) {
160
+ return new Promise((resolve) => setTimeout(resolve, ms));
161
+ }
@@ -0,0 +1,72 @@
1
+ const TERMINAL_STATUSES = new Set(["SUCCESS", "FAILED", "CANCELLED"]);
2
+ export function summarizeRun(run) {
3
+ const eventStatus = [...run.events]
4
+ .reverse()
5
+ .map((event) => event.payload.status)
6
+ .find((status) => typeof status === "string");
7
+ const status = run.task?.status ?? eventStatus ?? "UNKNOWN";
8
+ const failureText = status === "FAILED" ? run.task?.message ?? latestEventMessage(run) ?? "" : "";
9
+ return {
10
+ runId: run.runId,
11
+ taskId: run.taskId,
12
+ status,
13
+ liveUrl: run.liveUrl,
14
+ updatedAt: run.updatedAt,
15
+ artifacts: run.artifacts,
16
+ failureAnalysis: TERMINAL_STATUSES.has(status) ? analyzeRunFailure(failureText, status) : runningAnalysis(status),
17
+ };
18
+ }
19
+ export function analyzeRunFailure(message, status = "FAILED") {
20
+ if (status === "SUCCESS") {
21
+ return { category: "none", summary: "No failure.", recommendation: "No retry needed." };
22
+ }
23
+ if (status === "CANCELLED") {
24
+ return {
25
+ category: "agent-or-runtime",
26
+ summary: "The run was cancelled before completion.",
27
+ recommendation: "Start a new run after confirming the device is free.",
28
+ };
29
+ }
30
+ const lower = message.toLowerCase();
31
+ // timeout must be checked before adb — the Agent's error message often
32
+ // includes ADB debug log lines (e.g. "dbug ADB Running 'adb -H ...'")
33
+ // concatenated with a timeout suffix. If "adb" is checked first these
34
+ // timeouts are misclassified as device-or-environment.
35
+ if (lower.includes("timeout") || lower.includes("api key") || lower.includes("midscene") || lower.includes("spawn")) {
36
+ return {
37
+ category: "agent-or-runtime",
38
+ summary: message || "The run failed in the agent or Midscene runtime.",
39
+ recommendation: "Run doctor, fix blocking runtime checks, then rerun.",
40
+ };
41
+ }
42
+ if (lower.includes("adb") ||
43
+ lower.includes("hdc") ||
44
+ lower.includes("wda") ||
45
+ lower.includes("iproxy") ||
46
+ lower.includes("device offline") ||
47
+ lower.includes("no device")) {
48
+ return {
49
+ category: "device-or-environment",
50
+ summary: message || "The run failed in device or environment setup.",
51
+ recommendation: "Check the device connection, adb/hdc/WDA availability, then rerun the same test.",
52
+ };
53
+ }
54
+ return {
55
+ category: "test-or-app-behavior",
56
+ summary: message || "The run failed while executing the generated test steps.",
57
+ recommendation: "Inspect the live viewer/report, adjust the test steps if they were too brittle, or file a product bug if the app behavior is wrong.",
58
+ };
59
+ }
60
+ function runningAnalysis(status) {
61
+ return {
62
+ category: "none",
63
+ summary: `Run is still ${status}.`,
64
+ recommendation: "Continue watching the run before changing the test case.",
65
+ };
66
+ }
67
+ function latestEventMessage(run) {
68
+ return [...run.events]
69
+ .reverse()
70
+ .map((event) => event.payload.message)
71
+ .find((message) => typeof message === "string");
72
+ }
@@ -0,0 +1,44 @@
1
+ import { spawn } from "node:child_process";
2
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ export async function installLocalRuntime(options) {
6
+ const runtimeRoot = options.runtimeRoot ?? join(homedir(), ".preflight", "runtime");
7
+ await runNpm(["run", "build"], options.projectRoot);
8
+ await rm(runtimeRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 });
9
+ await mkdir(runtimeRoot, { recursive: true });
10
+ await mkdir(join(runtimeRoot, "node", "bin"), { recursive: true });
11
+ await cp(process.execPath, join(runtimeRoot, "node", "bin", "node"));
12
+ await cp(join(options.projectRoot, "dist"), join(runtimeRoot, "dist"), { recursive: true });
13
+ await cp(join(options.projectRoot, "package.json"), join(runtimeRoot, "package.json"));
14
+ await cp(join(options.projectRoot, "package-lock.json"), join(runtimeRoot, "package-lock.json"));
15
+ await cp(join(options.projectRoot, "scripts"), join(runtimeRoot, "scripts"), { recursive: true }).catch(() => { });
16
+ await mkdir(join(runtimeRoot, "docs"), { recursive: true });
17
+ await cp(join(options.projectRoot, "docs", "visual-flow-ir-llm.md"), join(runtimeRoot, "docs", "visual-flow-ir-llm.md"));
18
+ await runNpm(["ci", "--omit=dev"], runtimeRoot);
19
+ await writeFile(join(runtimeRoot, "preflight-runtime.json"), `${JSON.stringify({ installedAt: new Date().toISOString(), source: options.projectRoot }, null, 2)}\n`, "utf8");
20
+ return {
21
+ runtimeRoot,
22
+ nodeBin: join(runtimeRoot, "node", "bin", "node"),
23
+ mcpEntry: join(runtimeRoot, "dist", "mcp", "cli.js"),
24
+ };
25
+ }
26
+ function run(command, args, cwd) {
27
+ return new Promise((resolve, reject) => {
28
+ const child = spawn(command, args, { cwd, stdio: "inherit" });
29
+ child.on("error", reject);
30
+ child.on("close", (code) => {
31
+ if (code === 0)
32
+ resolve();
33
+ else
34
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? "unknown"}`));
35
+ });
36
+ });
37
+ }
38
+ function runNpm(args, cwd) {
39
+ const npmExecPath = process.env.npm_execpath;
40
+ if (npmExecPath) {
41
+ return run(process.execPath, [npmExecPath, ...args], cwd);
42
+ }
43
+ return run("npm", args, cwd);
44
+ }