@web-auto/camo 0.1.26 → 0.2.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.
- package/LICENSE +21 -21
- package/README.md +586 -586
- package/bin/browser-service.mjs +11 -11
- package/bin/camo.mjs +22 -22
- package/package.json +48 -48
- package/scripts/build.mjs +19 -19
- package/scripts/bump-version.mjs +34 -34
- package/scripts/check-file-size.mjs +80 -80
- package/scripts/file-size-policy.json +12 -2
- package/scripts/install.mjs +76 -76
- package/scripts/release.sh +54 -54
- package/src/autoscript/action-providers/index.mjs +6 -6
- package/src/autoscript/impact-engine.mjs +78 -78
- package/src/autoscript/runtime.mjs +1017 -1017
- package/src/autoscript/schema.mjs +376 -376
- package/src/cli.mjs +405 -405
- package/src/commands/attach.mjs +141 -141
- package/src/commands/autoscript.mjs +1011 -1011
- package/src/commands/browser.mjs +1255 -1257
- package/src/commands/container.mjs +401 -401
- package/src/commands/cookies.mjs +69 -69
- package/src/commands/create.mjs +98 -98
- package/src/commands/devtools.mjs +349 -349
- package/src/commands/events.mjs +152 -152
- package/src/commands/highlight-mode.mjs +24 -24
- package/src/commands/init.mjs +68 -68
- package/src/commands/lifecycle.mjs +275 -275
- package/src/commands/mouse.mjs +45 -45
- package/src/commands/profile.mjs +46 -46
- package/src/commands/record.mjs +115 -115
- package/src/commands/system.mjs +14 -14
- package/src/commands/window.mjs +123 -123
- package/src/container/change-notifier.mjs +362 -362
- package/src/container/element-filter.mjs +143 -143
- package/src/container/index.mjs +3 -3
- package/src/container/runtime-core/checkpoint.mjs +209 -209
- package/src/container/runtime-core/index.mjs +21 -21
- package/src/container/runtime-core/operations/index.mjs +774 -774
- package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
- package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
- package/src/container/runtime-core/operations/viewport.mjs +189 -189
- package/src/container/runtime-core/search.mjs +190 -190
- package/src/container/runtime-core/subscription.mjs +224 -224
- package/src/container/runtime-core/utils.mjs +94 -94
- package/src/container/runtime-core/validation.mjs +127 -184
- package/src/container/runtime-core.mjs +1 -1
- package/src/container/subscription-registry.mjs +459 -459
- package/src/core/actions.mjs +561 -561
- package/src/core/browser.mjs +266 -266
- package/src/core/index.mjs +52 -52
- package/src/core/utils.mjs +91 -91
- package/src/events/daemon-entry.mjs +33 -33
- package/src/events/daemon.mjs +80 -80
- package/src/events/progress-log.mjs +109 -109
- package/src/events/ws-server.mjs +239 -239
- package/src/lib/client.mjs +200 -200
- package/src/lifecycle/cleanup.mjs +83 -83
- package/src/lifecycle/lock.mjs +126 -126
- package/src/lifecycle/session-registry.mjs +279 -279
- package/src/lifecycle/session-view.mjs +76 -76
- package/src/lifecycle/session-watchdog.mjs +281 -281
- package/src/services/browser-service/index.js +671 -674
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -336
- package/src/services/browser-service/internal/ElementRegistry.js +60 -60
- package/src/services/browser-service/internal/ProfileLock.js +84 -84
- package/src/services/browser-service/internal/SessionManager.js +184 -184
- package/src/services/browser-service/internal/SessionManager.test.js +39 -39
- package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
- package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
- package/src/services/browser-service/internal/browser-session/logging.js +46 -46
- package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
- package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
- package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
- package/src/services/browser-service/internal/browser-session/recording.js +198 -198
- package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
- package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
- package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
- package/src/services/browser-service/internal/browser-session/types.js +14 -14
- package/src/services/browser-service/internal/browser-session/utils.js +95 -95
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
- package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
- package/src/services/browser-service/internal/container-matcher.js +851 -851
- package/src/services/browser-service/internal/container-registry.js +182 -182
- package/src/services/browser-service/internal/engine-manager.js +259 -259
- package/src/services/browser-service/internal/fingerprint.js +203 -203
- package/src/services/browser-service/internal/heartbeat.js +137 -137
- package/src/services/browser-service/internal/logging.js +46 -46
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
- package/src/services/browser-service/internal/pageRuntime.js +28 -28
- package/src/services/browser-service/internal/runtimeInjector.js +31 -31
- package/src/services/browser-service/internal/service-process-logger.js +140 -140
- package/src/services/browser-service/internal/state-bus.js +45 -45
- package/src/services/browser-service/internal/storage-paths.js +42 -42
- package/src/services/browser-service/internal/ws-server.js +1194 -1194
- package/src/services/browser-service/internal/ws-server.test.js +58 -58
- package/src/services/browser-service/server.mjs +6 -6
- package/src/services/controller/cli-bridge.js +93 -93
- package/src/services/controller/container-index.js +50 -50
- package/src/services/controller/container-storage.js +36 -36
- package/src/services/controller/controller-actions.js +207 -207
- package/src/services/controller/controller.js +1138 -1138
- package/src/services/controller/selectors.js +54 -54
- package/src/services/controller/transport.js +125 -125
- package/src/utils/args.mjs +26 -26
- package/src/utils/browser-service.mjs +544 -544
- package/src/utils/command-log.mjs +64 -64
- package/src/utils/config.mjs +214 -214
- package/src/utils/fingerprint.mjs +181 -181
- package/src/utils/help.mjs +216 -216
- package/src/utils/js-policy.mjs +13 -13
- package/src/utils/ws-client.mjs +30 -30
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
- package/src/services/browser-service/index.js.bak +0 -671
|
@@ -1,1011 +1,1011 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
|
|
4
|
-
import { explainAutoscript, loadAndValidateAutoscript } from '../autoscript/schema.mjs';
|
|
5
|
-
import { AutoscriptRunner } from '../autoscript/runtime.mjs';
|
|
6
|
-
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
7
|
-
|
|
8
|
-
function readFlagValue(args, names) {
|
|
9
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
10
|
-
if (!names.includes(args[i])) continue;
|
|
11
|
-
const value = args[i + 1];
|
|
12
|
-
if (!value || String(value).startsWith('-')) return null;
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function collectPositionals(args, startIndex = 2, valueFlags = new Set(['--profile', '-p'])) {
|
|
19
|
-
const out = [];
|
|
20
|
-
for (let i = startIndex; i < args.length; i += 1) {
|
|
21
|
-
const arg = args[i];
|
|
22
|
-
if (!arg) continue;
|
|
23
|
-
if (valueFlags.has(arg)) {
|
|
24
|
-
i += 1;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (String(arg).startsWith('-')) continue;
|
|
28
|
-
out.push(arg);
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function assertExistingProfile(profileId) {
|
|
34
|
-
const id = String(profileId || '').trim();
|
|
35
|
-
if (!id) {
|
|
36
|
-
throw new Error('profileId is required');
|
|
37
|
-
}
|
|
38
|
-
const known = new Set(listProfiles());
|
|
39
|
-
if (!known.has(id)) {
|
|
40
|
-
throw new Error(`profile not found: ${id}. create it first with "camo profile create ${id}"`);
|
|
41
|
-
}
|
|
42
|
-
return id;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function appendJsonLine(filePath, payload) {
|
|
46
|
-
if (!filePath) return;
|
|
47
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
-
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeResultPayload(result) {
|
|
52
|
-
if (!result || typeof result !== 'object') return null;
|
|
53
|
-
if (result.result && typeof result.result === 'object' && !Array.isArray(result.result)) {
|
|
54
|
-
return result.result;
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function bumpCounter(target, key) {
|
|
60
|
-
if (!target[key]) {
|
|
61
|
-
target[key] = 1;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
target[key] += 1;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function createRunSummaryTracker({ file, profileId, runId }) {
|
|
68
|
-
return {
|
|
69
|
-
file,
|
|
70
|
-
profileId,
|
|
71
|
-
runId,
|
|
72
|
-
runName: null,
|
|
73
|
-
startTs: null,
|
|
74
|
-
stopTs: null,
|
|
75
|
-
stopReason: null,
|
|
76
|
-
counts: {
|
|
77
|
-
operationStart: 0,
|
|
78
|
-
operationDone: 0,
|
|
79
|
-
operationError: 0,
|
|
80
|
-
operationSkipped: 0,
|
|
81
|
-
operationTerminal: 0,
|
|
82
|
-
watchError: 0,
|
|
83
|
-
},
|
|
84
|
-
target: {
|
|
85
|
-
visitedMax: 0,
|
|
86
|
-
notesClosed: 0,
|
|
87
|
-
harvestCount: 0,
|
|
88
|
-
likeOps: 0,
|
|
89
|
-
},
|
|
90
|
-
comments: {
|
|
91
|
-
total: 0,
|
|
92
|
-
expectedTotal: 0,
|
|
93
|
-
bottomReached: 0,
|
|
94
|
-
recoveriesTotal: 0,
|
|
95
|
-
coverageSum: 0,
|
|
96
|
-
coverageCount: 0,
|
|
97
|
-
exitReasonCounts: {},
|
|
98
|
-
commentsPathsCount: 0,
|
|
99
|
-
},
|
|
100
|
-
likes: {
|
|
101
|
-
scanned: 0,
|
|
102
|
-
hit: 0,
|
|
103
|
-
liked: 0,
|
|
104
|
-
dedupSkipped: 0,
|
|
105
|
-
alreadyLikedSkipped: 0,
|
|
106
|
-
verifyFailed: 0,
|
|
107
|
-
clickFailed: 0,
|
|
108
|
-
missingLikeControl: 0,
|
|
109
|
-
summaryCount: 0,
|
|
110
|
-
},
|
|
111
|
-
closeDetail: {
|
|
112
|
-
rollbackTotal: 0,
|
|
113
|
-
returnToSearchTotal: 0,
|
|
114
|
-
searchCountMax: 0,
|
|
115
|
-
pageExitReasonCounts: {},
|
|
116
|
-
},
|
|
117
|
-
terminal: {
|
|
118
|
-
operationId: null,
|
|
119
|
-
code: null,
|
|
120
|
-
ts: null,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function applyRunSummaryEvent(tracker, payload) {
|
|
126
|
-
if (!payload || typeof payload !== 'object') return;
|
|
127
|
-
const { event, ts } = payload;
|
|
128
|
-
if (event === 'autoscript:start') {
|
|
129
|
-
tracker.startTs = tracker.startTs || ts || null;
|
|
130
|
-
tracker.runId = tracker.runId || payload.runId || null;
|
|
131
|
-
tracker.profileId = tracker.profileId || payload.profileId || null;
|
|
132
|
-
tracker.runName = payload.name || tracker.runName;
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (event === 'autoscript:stop') {
|
|
136
|
-
tracker.stopReason = payload.reason || tracker.stopReason;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (event === 'autoscript:watch_error') {
|
|
140
|
-
tracker.counts.watchError += 1;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (event === 'autoscript:operation_start') {
|
|
144
|
-
tracker.counts.operationStart += 1;
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (event === 'autoscript:operation_error') {
|
|
148
|
-
tracker.counts.operationError += 1;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (event === 'autoscript:operation_skipped') {
|
|
152
|
-
tracker.counts.operationSkipped += 1;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (event === 'autoscript:operation_terminal') {
|
|
156
|
-
tracker.counts.operationTerminal += 1;
|
|
157
|
-
tracker.terminal.operationId = payload.operationId || tracker.terminal.operationId;
|
|
158
|
-
tracker.terminal.code = payload.code || tracker.terminal.code;
|
|
159
|
-
tracker.terminal.ts = payload.ts || tracker.terminal.ts;
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (event !== 'autoscript:operation_done') return;
|
|
163
|
-
|
|
164
|
-
tracker.counts.operationDone += 1;
|
|
165
|
-
const result = normalizeResultPayload(payload.result);
|
|
166
|
-
const opId = payload.operationId;
|
|
167
|
-
if (!opId || !result || typeof result !== 'object') return;
|
|
168
|
-
|
|
169
|
-
if (opId === 'open_first_detail' || opId === 'open_next_detail') {
|
|
170
|
-
const visited = Number(result.visited);
|
|
171
|
-
if (Number.isFinite(visited)) {
|
|
172
|
-
tracker.target.visitedMax = Math.max(tracker.target.visitedMax, visited);
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (opId === 'comments_harvest') {
|
|
178
|
-
tracker.target.harvestCount += 1;
|
|
179
|
-
tracker.comments.total += Number(result.collected || 0);
|
|
180
|
-
tracker.comments.expectedTotal += Number(result.expectedCommentsCount || 0);
|
|
181
|
-
tracker.comments.recoveriesTotal += Number(result.recoveries || 0);
|
|
182
|
-
if (result.reachedBottom === true) tracker.comments.bottomReached += 1;
|
|
183
|
-
if (result.commentsPath) tracker.comments.commentsPathsCount += 1;
|
|
184
|
-
const coverage = Number(result.commentCoverageRate);
|
|
185
|
-
if (Number.isFinite(coverage)) {
|
|
186
|
-
tracker.comments.coverageCount += 1;
|
|
187
|
-
tracker.comments.coverageSum += coverage;
|
|
188
|
-
}
|
|
189
|
-
bumpCounter(tracker.comments.exitReasonCounts, result.exitReason || 'unknown');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (opId === 'comment_like') {
|
|
194
|
-
tracker.target.likeOps += 1;
|
|
195
|
-
tracker.likes.scanned += Number(result.scannedCount || 0);
|
|
196
|
-
tracker.likes.hit += Number(result.hitCount || 0);
|
|
197
|
-
tracker.likes.liked += Number(result.likedCount || 0);
|
|
198
|
-
tracker.likes.dedupSkipped += Number(result.dedupSkipped || 0);
|
|
199
|
-
tracker.likes.alreadyLikedSkipped += Number(result.alreadyLikedSkipped || 0);
|
|
200
|
-
tracker.likes.verifyFailed += Number(result.verifyFailed || 0);
|
|
201
|
-
tracker.likes.clickFailed += Number(result.clickFailed || 0);
|
|
202
|
-
tracker.likes.missingLikeControl += Number(result.missingLikeControl || 0);
|
|
203
|
-
if (result.summaryPath) tracker.likes.summaryCount += 1;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (opId === 'close_detail') {
|
|
208
|
-
tracker.target.notesClosed += 1;
|
|
209
|
-
tracker.closeDetail.rollbackTotal += Number(result.rollbackCount || 0);
|
|
210
|
-
tracker.closeDetail.returnToSearchTotal += Number(result.returnToSearchCount || 0);
|
|
211
|
-
tracker.closeDetail.searchCountMax = Math.max(
|
|
212
|
-
tracker.closeDetail.searchCountMax,
|
|
213
|
-
Number(result.searchCount || 0),
|
|
214
|
-
);
|
|
215
|
-
bumpCounter(tracker.closeDetail.pageExitReasonCounts, result.pageExitReason || 'unknown');
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function buildRunSummary(tracker, fallbackStopReason = null) {
|
|
220
|
-
const harvestCount = tracker.target.harvestCount;
|
|
221
|
-
const coverageCount = tracker.comments.coverageCount;
|
|
222
|
-
const durationSec = tracker.startTs && tracker.stopTs
|
|
223
|
-
? (new Date(tracker.stopTs).getTime() - new Date(tracker.startTs).getTime()) / 1000
|
|
224
|
-
: null;
|
|
225
|
-
return {
|
|
226
|
-
runId: tracker.runId || null,
|
|
227
|
-
profileId: tracker.profileId || null,
|
|
228
|
-
file: tracker.file || null,
|
|
229
|
-
runName: tracker.runName || null,
|
|
230
|
-
startTs: tracker.startTs || null,
|
|
231
|
-
stopTs: tracker.stopTs || null,
|
|
232
|
-
durationSec,
|
|
233
|
-
stopReason: tracker.stopReason || fallbackStopReason || null,
|
|
234
|
-
counts: tracker.counts,
|
|
235
|
-
target: tracker.target,
|
|
236
|
-
comments: {
|
|
237
|
-
total: tracker.comments.total,
|
|
238
|
-
expectedTotal: tracker.comments.expectedTotal,
|
|
239
|
-
avgPerNote: harvestCount > 0 ? tracker.comments.total / harvestCount : null,
|
|
240
|
-
bottomReached: tracker.comments.bottomReached,
|
|
241
|
-
bottomRate: harvestCount > 0 ? tracker.comments.bottomReached / harvestCount : null,
|
|
242
|
-
recoveriesTotal: tracker.comments.recoveriesTotal,
|
|
243
|
-
exitReasonCounts: tracker.comments.exitReasonCounts,
|
|
244
|
-
coverageAvg: coverageCount > 0 ? tracker.comments.coverageSum / coverageCount : null,
|
|
245
|
-
commentsPathsCount: tracker.comments.commentsPathsCount,
|
|
246
|
-
},
|
|
247
|
-
likes: tracker.likes,
|
|
248
|
-
closeDetail: tracker.closeDetail,
|
|
249
|
-
terminal: tracker.terminal.code
|
|
250
|
-
? tracker.terminal
|
|
251
|
-
: null,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function buildDefaultSummaryPath(jsonlPath) {
|
|
256
|
-
if (!jsonlPath) return null;
|
|
257
|
-
return jsonlPath.endsWith('.jsonl')
|
|
258
|
-
? `${jsonlPath.slice(0, -'.jsonl'.length)}.summary.json`
|
|
259
|
-
: `${jsonlPath}.summary.json`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function readJsonlEvents(filePath) {
|
|
263
|
-
const resolved = path.resolve(filePath);
|
|
264
|
-
if (!fs.existsSync(resolved)) {
|
|
265
|
-
throw new Error(`JSONL file not found: ${resolved}`);
|
|
266
|
-
}
|
|
267
|
-
const raw = fs.readFileSync(resolved, 'utf8');
|
|
268
|
-
const rows = raw
|
|
269
|
-
.split('\n')
|
|
270
|
-
.map((line) => line.trim())
|
|
271
|
-
.filter(Boolean)
|
|
272
|
-
.map((line, index) => {
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(line);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
throw new Error(`Invalid JSONL at line ${index + 1}: ${err?.message || String(err)}`);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
return { resolvedPath: resolved, rows };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function cloneObject(value, fallback = {}) {
|
|
283
|
-
if (!value || typeof value !== 'object') return { ...fallback };
|
|
284
|
-
return { ...fallback, ...value };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function reduceSubscriptionState(subscriptionState, event) {
|
|
288
|
-
if (!event?.subscriptionId) return;
|
|
289
|
-
const prev = subscriptionState[event.subscriptionId] || {
|
|
290
|
-
exists: false,
|
|
291
|
-
appearCount: 0,
|
|
292
|
-
version: 0,
|
|
293
|
-
lastEventAt: null,
|
|
294
|
-
};
|
|
295
|
-
const next = { ...prev, lastEventAt: event.ts || null };
|
|
296
|
-
if (event.type === 'appear') {
|
|
297
|
-
next.exists = true;
|
|
298
|
-
next.appearCount = Number(prev.appearCount || 0) + 1;
|
|
299
|
-
next.version = Number(prev.version || 0) + 1;
|
|
300
|
-
} else if (event.type === 'exist') {
|
|
301
|
-
next.exists = true;
|
|
302
|
-
} else if (event.type === 'disappear') {
|
|
303
|
-
next.exists = false;
|
|
304
|
-
next.version = Number(prev.version || 0) + 1;
|
|
305
|
-
} else if (event.type === 'change') {
|
|
306
|
-
next.exists = Number(event.count || 0) > 0 || prev.exists === true;
|
|
307
|
-
next.version = Number(prev.version || 0) + 1;
|
|
308
|
-
}
|
|
309
|
-
subscriptionState[event.subscriptionId] = next;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function buildSnapshotFromEvents({
|
|
313
|
-
events,
|
|
314
|
-
file,
|
|
315
|
-
reason = 'log_snapshot',
|
|
316
|
-
}) {
|
|
317
|
-
const first = events[0] || null;
|
|
318
|
-
const last = events[events.length - 1] || null;
|
|
319
|
-
const tracker = createRunSummaryTracker({
|
|
320
|
-
file,
|
|
321
|
-
profileId: first?.profileId || null,
|
|
322
|
-
runId: first?.runId || null,
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const operationState = {};
|
|
326
|
-
const subscriptionState = {};
|
|
327
|
-
let state = { active: false, reason: null, startedAt: null, stoppedAt: null };
|
|
328
|
-
let lastEvent = null;
|
|
329
|
-
|
|
330
|
-
for (const event of events) {
|
|
331
|
-
applyRunSummaryEvent(tracker, event);
|
|
332
|
-
|
|
333
|
-
if (event.event === 'autoscript:start') {
|
|
334
|
-
state = {
|
|
335
|
-
active: true,
|
|
336
|
-
reason: null,
|
|
337
|
-
startedAt: event.ts || null,
|
|
338
|
-
stoppedAt: null,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (event.event === 'autoscript:event') {
|
|
343
|
-
reduceSubscriptionState(subscriptionState, event);
|
|
344
|
-
lastEvent = {
|
|
345
|
-
type: event.type || 'tick',
|
|
346
|
-
subscriptionId: event.subscriptionId || null,
|
|
347
|
-
selector: event.selector || null,
|
|
348
|
-
count: event.count ?? null,
|
|
349
|
-
timestamp: event.ts || null,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (event.event === 'autoscript:operation_start') {
|
|
354
|
-
const prev = cloneObject(operationState[event.operationId], {
|
|
355
|
-
status: 'pending',
|
|
356
|
-
runs: 0,
|
|
357
|
-
lastError: null,
|
|
358
|
-
updatedAt: null,
|
|
359
|
-
result: null,
|
|
360
|
-
});
|
|
361
|
-
operationState[event.operationId] = {
|
|
362
|
-
...prev,
|
|
363
|
-
status: 'running',
|
|
364
|
-
updatedAt: event.ts || prev.updatedAt,
|
|
365
|
-
};
|
|
366
|
-
} else if (event.event === 'autoscript:operation_done') {
|
|
367
|
-
const prev = cloneObject(operationState[event.operationId], {
|
|
368
|
-
status: 'pending',
|
|
369
|
-
runs: 0,
|
|
370
|
-
lastError: null,
|
|
371
|
-
updatedAt: null,
|
|
372
|
-
result: null,
|
|
373
|
-
});
|
|
374
|
-
operationState[event.operationId] = {
|
|
375
|
-
...prev,
|
|
376
|
-
status: 'done',
|
|
377
|
-
runs: Number(prev.runs || 0) + 1,
|
|
378
|
-
lastError: null,
|
|
379
|
-
updatedAt: event.ts || prev.updatedAt,
|
|
380
|
-
result: event.result ?? null,
|
|
381
|
-
};
|
|
382
|
-
} else if (event.event === 'autoscript:operation_error') {
|
|
383
|
-
const prev = cloneObject(operationState[event.operationId], {
|
|
384
|
-
status: 'pending',
|
|
385
|
-
runs: 0,
|
|
386
|
-
lastError: null,
|
|
387
|
-
updatedAt: null,
|
|
388
|
-
result: null,
|
|
389
|
-
});
|
|
390
|
-
operationState[event.operationId] = {
|
|
391
|
-
...prev,
|
|
392
|
-
status: 'failed',
|
|
393
|
-
runs: Number(prev.runs || 0) + 1,
|
|
394
|
-
lastError: event.message || event.code || 'operation failed',
|
|
395
|
-
updatedAt: event.ts || prev.updatedAt,
|
|
396
|
-
result: null,
|
|
397
|
-
};
|
|
398
|
-
} else if (event.event === 'autoscript:operation_skipped') {
|
|
399
|
-
const prev = cloneObject(operationState[event.operationId], {
|
|
400
|
-
status: 'pending',
|
|
401
|
-
runs: 0,
|
|
402
|
-
lastError: null,
|
|
403
|
-
updatedAt: null,
|
|
404
|
-
result: null,
|
|
405
|
-
});
|
|
406
|
-
operationState[event.operationId] = {
|
|
407
|
-
...prev,
|
|
408
|
-
status: 'skipped',
|
|
409
|
-
runs: Number(prev.runs || 0) + 1,
|
|
410
|
-
lastError: null,
|
|
411
|
-
updatedAt: event.ts || prev.updatedAt,
|
|
412
|
-
result: {
|
|
413
|
-
code: event.code || null,
|
|
414
|
-
reason: event.reason || null,
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
} else if (event.event === 'autoscript:operation_terminal') {
|
|
418
|
-
const prev = cloneObject(operationState[event.operationId], {
|
|
419
|
-
status: 'pending',
|
|
420
|
-
runs: 0,
|
|
421
|
-
lastError: null,
|
|
422
|
-
updatedAt: null,
|
|
423
|
-
result: null,
|
|
424
|
-
});
|
|
425
|
-
operationState[event.operationId] = {
|
|
426
|
-
...prev,
|
|
427
|
-
status: 'done',
|
|
428
|
-
runs: Number(prev.runs || 0) + 1,
|
|
429
|
-
lastError: null,
|
|
430
|
-
updatedAt: event.ts || prev.updatedAt,
|
|
431
|
-
result: {
|
|
432
|
-
terminalDoneCode: event.code || null,
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
} else if (event.event === 'autoscript:stop') {
|
|
436
|
-
state = {
|
|
437
|
-
...state,
|
|
438
|
-
active: false,
|
|
439
|
-
reason: event.reason || state.reason || null,
|
|
440
|
-
stoppedAt: event.ts || state.stoppedAt || null,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
tracker.stopTs = last?.ts || tracker.stopTs || null;
|
|
446
|
-
const summary = buildRunSummary(tracker, state.reason || null);
|
|
447
|
-
const snapshot = {
|
|
448
|
-
kind: 'autoscript_snapshot',
|
|
449
|
-
version: 1,
|
|
450
|
-
reason,
|
|
451
|
-
createdAt: new Date().toISOString(),
|
|
452
|
-
sourceJsonl: file,
|
|
453
|
-
runId: summary.runId || null,
|
|
454
|
-
profileId: summary.profileId || null,
|
|
455
|
-
scriptName: summary.runName || null,
|
|
456
|
-
summary,
|
|
457
|
-
state: {
|
|
458
|
-
state,
|
|
459
|
-
subscriptionState,
|
|
460
|
-
operationState,
|
|
461
|
-
operationScheduleState: {},
|
|
462
|
-
runtimeContext: { vars: {}, tabPool: null, currentTab: null },
|
|
463
|
-
lastNavigationAt: 0,
|
|
464
|
-
lastEvent,
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
return snapshot;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function toOperationOrder(script) {
|
|
471
|
-
return Array.isArray(script?.operations)
|
|
472
|
-
? script.operations.map((op) => String(op?.id || '').trim()).filter(Boolean)
|
|
473
|
-
: [];
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function buildDescendantSet(script, nodeId) {
|
|
477
|
-
const dependents = new Map();
|
|
478
|
-
for (const op of script.operations || []) {
|
|
479
|
-
for (const dep of op.dependsOn || []) {
|
|
480
|
-
if (!dependents.has(dep)) dependents.set(dep, new Set());
|
|
481
|
-
dependents.get(dep).add(op.id);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
const seen = new Set([nodeId]);
|
|
485
|
-
const queue = [nodeId];
|
|
486
|
-
while (queue.length > 0) {
|
|
487
|
-
const current = queue.shift();
|
|
488
|
-
for (const next of dependents.get(current) || []) {
|
|
489
|
-
if (seen.has(next)) continue;
|
|
490
|
-
seen.add(next);
|
|
491
|
-
queue.push(next);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return seen;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function buildResumeStateFromSnapshot(script, snapshot, fromNodeId = null) {
|
|
498
|
-
const sourceState = snapshot?.state && typeof snapshot.state === 'object'
|
|
499
|
-
? snapshot.state
|
|
500
|
-
: {};
|
|
501
|
-
const sourceOps = sourceState.operationState && typeof sourceState.operationState === 'object'
|
|
502
|
-
? sourceState.operationState
|
|
503
|
-
: {};
|
|
504
|
-
const forceRunOperationIds = [];
|
|
505
|
-
const operationState = {};
|
|
506
|
-
const descendants = fromNodeId ? buildDescendantSet(script, fromNodeId) : null;
|
|
507
|
-
|
|
508
|
-
for (const opId of toOperationOrder(script)) {
|
|
509
|
-
const prev = sourceOps[opId] && typeof sourceOps[opId] === 'object'
|
|
510
|
-
? sourceOps[opId]
|
|
511
|
-
: {
|
|
512
|
-
status: 'pending',
|
|
513
|
-
runs: 0,
|
|
514
|
-
lastError: null,
|
|
515
|
-
updatedAt: null,
|
|
516
|
-
result: null,
|
|
517
|
-
};
|
|
518
|
-
if (!descendants) {
|
|
519
|
-
operationState[opId] = {
|
|
520
|
-
status: String(prev.status || 'pending'),
|
|
521
|
-
runs: Math.max(0, Number(prev.runs || 0) || 0),
|
|
522
|
-
lastError: prev.lastError || null,
|
|
523
|
-
updatedAt: prev.updatedAt || null,
|
|
524
|
-
result: prev.result ?? null,
|
|
525
|
-
};
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (descendants.has(opId)) {
|
|
530
|
-
operationState[opId] = {
|
|
531
|
-
status: 'pending',
|
|
532
|
-
runs: Math.max(0, Number(prev.runs || 0) || 0),
|
|
533
|
-
lastError: null,
|
|
534
|
-
updatedAt: prev.updatedAt || null,
|
|
535
|
-
result: null,
|
|
536
|
-
};
|
|
537
|
-
forceRunOperationIds.push(opId);
|
|
538
|
-
} else {
|
|
539
|
-
operationState[opId] = {
|
|
540
|
-
status: 'done',
|
|
541
|
-
runs: Math.max(1, Number(prev.runs || 1) || 1),
|
|
542
|
-
lastError: null,
|
|
543
|
-
updatedAt: prev.updatedAt || null,
|
|
544
|
-
result: prev.result ?? { code: 'RESUME_SKIPPED_PREVIOUS_BRANCH' },
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
initialState: {
|
|
551
|
-
state: {
|
|
552
|
-
state: sourceState.state || {
|
|
553
|
-
active: false,
|
|
554
|
-
reason: null,
|
|
555
|
-
startedAt: null,
|
|
556
|
-
stoppedAt: null,
|
|
557
|
-
},
|
|
558
|
-
subscriptionState: sourceState.subscriptionState || {},
|
|
559
|
-
operationState,
|
|
560
|
-
operationScheduleState: sourceState.operationScheduleState || {},
|
|
561
|
-
runtimeContext: sourceState.runtimeContext || { vars: {}, tabPool: null, currentTab: null },
|
|
562
|
-
lastNavigationAt: Number(sourceState.lastNavigationAt || 0) || 0,
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
forceRunOperationIds,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function createMockOperationExecutor(fixture) {
|
|
570
|
-
const source = fixture?.operations && typeof fixture.operations === 'object'
|
|
571
|
-
? fixture.operations
|
|
572
|
-
: {};
|
|
573
|
-
const queues = new Map(
|
|
574
|
-
Object.entries(source).map(([key, value]) => [key, Array.isArray(value) ? [...value] : [value]]),
|
|
575
|
-
);
|
|
576
|
-
const defaultResult = fixture?.defaultResult && typeof fixture.defaultResult === 'object'
|
|
577
|
-
? fixture.defaultResult
|
|
578
|
-
: { ok: true, code: 'OPERATION_DONE', message: 'mock operation done', data: { mock: true } };
|
|
579
|
-
|
|
580
|
-
return ({ operation }) => {
|
|
581
|
-
const key = String(operation?.id || '').trim();
|
|
582
|
-
const queue = queues.get(key);
|
|
583
|
-
const wildcard = queues.get('*');
|
|
584
|
-
const next = queue && queue.length > 0
|
|
585
|
-
? queue.shift()
|
|
586
|
-
: (wildcard && wildcard.length > 0 ? wildcard.shift() : defaultResult);
|
|
587
|
-
return next;
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function handleValidate(args) {
|
|
592
|
-
const filePath = collectPositionals(args)[0];
|
|
593
|
-
if (!filePath) {
|
|
594
|
-
throw new Error('Usage: camo autoscript validate <file>');
|
|
595
|
-
}
|
|
596
|
-
const { sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
597
|
-
console.log(JSON.stringify({
|
|
598
|
-
ok: validation.ok,
|
|
599
|
-
file: sourcePath,
|
|
600
|
-
errors: validation.errors,
|
|
601
|
-
warnings: validation.warnings,
|
|
602
|
-
operationOrder: validation.topologicalOrder,
|
|
603
|
-
}, null, 2));
|
|
604
|
-
if (!validation.ok) {
|
|
605
|
-
process.exitCode = 1;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
async function handleExplain(args) {
|
|
610
|
-
const filePath = collectPositionals(args)[0];
|
|
611
|
-
if (!filePath) {
|
|
612
|
-
throw new Error('Usage: camo autoscript explain <file>');
|
|
613
|
-
}
|
|
614
|
-
const { script, sourcePath } = loadAndValidateAutoscript(filePath);
|
|
615
|
-
const explained = explainAutoscript(script);
|
|
616
|
-
console.log(JSON.stringify({
|
|
617
|
-
ok: explained.ok,
|
|
618
|
-
file: sourcePath,
|
|
619
|
-
...explained,
|
|
620
|
-
}, null, 2));
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async function executeAutoscriptRuntime({
|
|
624
|
-
commandName,
|
|
625
|
-
script,
|
|
626
|
-
sourcePath,
|
|
627
|
-
profileId,
|
|
628
|
-
jsonlPath,
|
|
629
|
-
summaryPath,
|
|
630
|
-
runnerOptions = {},
|
|
631
|
-
extraStartPayload = {},
|
|
632
|
-
}) {
|
|
633
|
-
let jsonlWriteError = null;
|
|
634
|
-
const summaryTracker = createRunSummaryTracker({
|
|
635
|
-
file: sourcePath,
|
|
636
|
-
profileId,
|
|
637
|
-
runId: null,
|
|
638
|
-
});
|
|
639
|
-
const appendRunJsonl = (payload) => {
|
|
640
|
-
if (!jsonlPath) return;
|
|
641
|
-
if (jsonlWriteError) return;
|
|
642
|
-
try {
|
|
643
|
-
appendJsonLine(jsonlPath, payload);
|
|
644
|
-
} catch (err) {
|
|
645
|
-
jsonlWriteError = err;
|
|
646
|
-
console.error(JSON.stringify({
|
|
647
|
-
event: 'autoscript:jsonl_error',
|
|
648
|
-
file: jsonlPath,
|
|
649
|
-
message: err?.message || String(err),
|
|
650
|
-
}));
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
const runner = new AutoscriptRunner({
|
|
655
|
-
...script,
|
|
656
|
-
profileId,
|
|
657
|
-
}, {
|
|
658
|
-
profileId,
|
|
659
|
-
...runnerOptions,
|
|
660
|
-
log: (payload) => {
|
|
661
|
-
console.log(JSON.stringify(payload));
|
|
662
|
-
appendRunJsonl(payload);
|
|
663
|
-
applyRunSummaryEvent(summaryTracker, payload);
|
|
664
|
-
safeAppendProgressEvent({
|
|
665
|
-
source: 'autoscript.runtime',
|
|
666
|
-
mode: 'autoscript',
|
|
667
|
-
profileId: payload.profileId || profileId,
|
|
668
|
-
runId: payload.runId || null,
|
|
669
|
-
event: payload.event || 'autoscript.log',
|
|
670
|
-
payload,
|
|
671
|
-
});
|
|
672
|
-
},
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
const running = await runner.start();
|
|
676
|
-
summaryTracker.runId = running.runId;
|
|
677
|
-
safeAppendProgressEvent({
|
|
678
|
-
source: 'autoscript.command',
|
|
679
|
-
mode: 'autoscript',
|
|
680
|
-
profileId,
|
|
681
|
-
runId: running.runId,
|
|
682
|
-
event: `${commandName}.start`,
|
|
683
|
-
payload: {
|
|
684
|
-
file: sourcePath,
|
|
685
|
-
profileId,
|
|
686
|
-
runId: running.runId,
|
|
687
|
-
...extraStartPayload,
|
|
688
|
-
},
|
|
689
|
-
});
|
|
690
|
-
console.log(JSON.stringify({
|
|
691
|
-
ok: true,
|
|
692
|
-
command: commandName,
|
|
693
|
-
file: sourcePath,
|
|
694
|
-
profileId,
|
|
695
|
-
runId: running.runId,
|
|
696
|
-
message: 'Autoscript runtime started. Press Ctrl+C to stop.',
|
|
697
|
-
...extraStartPayload,
|
|
698
|
-
}, null, 2));
|
|
699
|
-
appendRunJsonl({
|
|
700
|
-
runId: running.runId,
|
|
701
|
-
profileId,
|
|
702
|
-
event: `${commandName}:run_start`,
|
|
703
|
-
ts: new Date().toISOString(),
|
|
704
|
-
file: sourcePath,
|
|
705
|
-
jsonlPath,
|
|
706
|
-
...extraStartPayload,
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
const onSigint = () => {
|
|
710
|
-
running.stop('signal_interrupt');
|
|
711
|
-
};
|
|
712
|
-
process.once('SIGINT', onSigint);
|
|
713
|
-
|
|
714
|
-
const done = await running.done.finally(() => {
|
|
715
|
-
process.removeListener('SIGINT', onSigint);
|
|
716
|
-
});
|
|
717
|
-
summaryTracker.stopTs = new Date().toISOString();
|
|
718
|
-
const summaryPayload = buildRunSummary(summaryTracker, done?.reason || null);
|
|
719
|
-
if (summaryPath) {
|
|
720
|
-
fs.mkdirSync(path.dirname(summaryPath), { recursive: true });
|
|
721
|
-
fs.writeFileSync(summaryPath, `${JSON.stringify(summaryPayload, null, 2)}\n`, 'utf8');
|
|
722
|
-
}
|
|
723
|
-
console.log(JSON.stringify({
|
|
724
|
-
event: 'autoscript:run_summary',
|
|
725
|
-
runId: running.runId,
|
|
726
|
-
profileId,
|
|
727
|
-
summaryPath,
|
|
728
|
-
summary: summaryPayload,
|
|
729
|
-
}, null, 2));
|
|
730
|
-
safeAppendProgressEvent({
|
|
731
|
-
source: 'autoscript.command',
|
|
732
|
-
mode: 'autoscript',
|
|
733
|
-
profileId,
|
|
734
|
-
runId: running.runId,
|
|
735
|
-
event: `${commandName}.stop`,
|
|
736
|
-
payload: {
|
|
737
|
-
file: sourcePath,
|
|
738
|
-
profileId,
|
|
739
|
-
runId: running.runId,
|
|
740
|
-
reason: done?.reason || null,
|
|
741
|
-
...extraStartPayload,
|
|
742
|
-
},
|
|
743
|
-
});
|
|
744
|
-
appendRunJsonl({
|
|
745
|
-
runId: running.runId,
|
|
746
|
-
profileId,
|
|
747
|
-
event: `${commandName}:run_stop`,
|
|
748
|
-
ts: new Date().toISOString(),
|
|
749
|
-
file: sourcePath,
|
|
750
|
-
reason: done?.reason || null,
|
|
751
|
-
...extraStartPayload,
|
|
752
|
-
});
|
|
753
|
-
appendRunJsonl({
|
|
754
|
-
runId: running.runId,
|
|
755
|
-
profileId,
|
|
756
|
-
event: `${commandName}:run_summary`,
|
|
757
|
-
ts: new Date().toISOString(),
|
|
758
|
-
summaryPath,
|
|
759
|
-
summary: summaryPayload,
|
|
760
|
-
...extraStartPayload,
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
return {
|
|
764
|
-
done,
|
|
765
|
-
summaryPath,
|
|
766
|
-
summary: summaryPayload,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
async function handleRun(args) {
|
|
771
|
-
const filePath = collectPositionals(args)[0];
|
|
772
|
-
if (!filePath) {
|
|
773
|
-
throw new Error('Usage: camo autoscript run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
774
|
-
}
|
|
775
|
-
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
776
|
-
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
777
|
-
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
778
|
-
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
779
|
-
const summaryPath = summaryPathRaw
|
|
780
|
-
? path.resolve(summaryPathRaw)
|
|
781
|
-
: buildDefaultSummaryPath(jsonlPath);
|
|
782
|
-
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
783
|
-
if (!validation.ok) {
|
|
784
|
-
console.log(JSON.stringify({
|
|
785
|
-
ok: false,
|
|
786
|
-
file: sourcePath,
|
|
787
|
-
errors: validation.errors,
|
|
788
|
-
warnings: validation.warnings,
|
|
789
|
-
}, null, 2));
|
|
790
|
-
process.exitCode = 1;
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const profileId = profileOverride || script.profileId || getDefaultProfile();
|
|
795
|
-
if (!profileId) {
|
|
796
|
-
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
797
|
-
}
|
|
798
|
-
assertExistingProfile(profileId);
|
|
799
|
-
|
|
800
|
-
await executeAutoscriptRuntime({
|
|
801
|
-
commandName: 'autoscript.run',
|
|
802
|
-
script,
|
|
803
|
-
sourcePath,
|
|
804
|
-
profileId,
|
|
805
|
-
jsonlPath,
|
|
806
|
-
summaryPath,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function readJsonFile(filePath) {
|
|
811
|
-
const resolved = path.resolve(filePath);
|
|
812
|
-
if (!fs.existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
813
|
-
try {
|
|
814
|
-
return {
|
|
815
|
-
resolvedPath: resolved,
|
|
816
|
-
payload: JSON.parse(fs.readFileSync(resolved, 'utf8')),
|
|
817
|
-
};
|
|
818
|
-
} catch (err) {
|
|
819
|
-
throw new Error(`Invalid JSON file: ${resolved} (${err?.message || String(err)})`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
async function handleSnapshot(args) {
|
|
824
|
-
const valueFlags = new Set(['--out', '-o']);
|
|
825
|
-
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
826
|
-
if (!filePath) {
|
|
827
|
-
throw new Error('Usage: camo autoscript snapshot <jsonl-file> [--out <snapshot-file>]');
|
|
828
|
-
}
|
|
829
|
-
const outRaw = readFlagValue(args, ['--out', '-o']);
|
|
830
|
-
const { resolvedPath, rows } = readJsonlEvents(filePath);
|
|
831
|
-
const snapshot = buildSnapshotFromEvents({ events: rows, file: resolvedPath });
|
|
832
|
-
const outputPath = outRaw
|
|
833
|
-
? path.resolve(outRaw)
|
|
834
|
-
: (resolvedPath.endsWith('.jsonl')
|
|
835
|
-
? `${resolvedPath.slice(0, -'.jsonl'.length)}.snapshot.json`
|
|
836
|
-
: `${resolvedPath}.snapshot.json`);
|
|
837
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
838
|
-
fs.writeFileSync(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
839
|
-
console.log(JSON.stringify({
|
|
840
|
-
ok: true,
|
|
841
|
-
command: 'autoscript.snapshot',
|
|
842
|
-
input: resolvedPath,
|
|
843
|
-
output: outputPath,
|
|
844
|
-
runId: snapshot.runId || null,
|
|
845
|
-
profileId: snapshot.profileId || null,
|
|
846
|
-
summary: snapshot.summary,
|
|
847
|
-
}, null, 2));
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
async function handleReplay(args) {
|
|
851
|
-
const valueFlags = new Set(['--summary-file', '--summary']);
|
|
852
|
-
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
853
|
-
if (!filePath) {
|
|
854
|
-
throw new Error('Usage: camo autoscript replay <jsonl-file> [--summary-file <path>]');
|
|
855
|
-
}
|
|
856
|
-
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
857
|
-
const { resolvedPath, rows } = readJsonlEvents(filePath);
|
|
858
|
-
const snapshot = buildSnapshotFromEvents({ events: rows, file: resolvedPath, reason: 'log_replay' });
|
|
859
|
-
const summaryPath = summaryPathRaw
|
|
860
|
-
? path.resolve(summaryPathRaw)
|
|
861
|
-
: buildDefaultSummaryPath(resolvedPath);
|
|
862
|
-
if (summaryPath) {
|
|
863
|
-
fs.mkdirSync(path.dirname(summaryPath), { recursive: true });
|
|
864
|
-
fs.writeFileSync(summaryPath, `${JSON.stringify(snapshot.summary, null, 2)}\n`, 'utf8');
|
|
865
|
-
}
|
|
866
|
-
console.log(JSON.stringify({
|
|
867
|
-
ok: true,
|
|
868
|
-
command: 'autoscript.replay',
|
|
869
|
-
input: resolvedPath,
|
|
870
|
-
summaryPath,
|
|
871
|
-
summary: snapshot.summary,
|
|
872
|
-
}, null, 2));
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
async function handleResume(args) {
|
|
876
|
-
const valueFlags = new Set(['--profile', '-p', '--snapshot', '--from-node', '--jsonl-file', '--jsonl', '--summary-file', '--summary']);
|
|
877
|
-
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
878
|
-
if (!filePath) {
|
|
879
|
-
throw new Error('Usage: camo autoscript resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
880
|
-
}
|
|
881
|
-
const snapshotPathRaw = readFlagValue(args, ['--snapshot']);
|
|
882
|
-
if (!snapshotPathRaw) {
|
|
883
|
-
throw new Error('autoscript resume requires --snapshot <snapshot-file>');
|
|
884
|
-
}
|
|
885
|
-
const fromNode = readFlagValue(args, ['--from-node']);
|
|
886
|
-
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
887
|
-
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
888
|
-
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
889
|
-
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
890
|
-
const summaryPath = summaryPathRaw ? path.resolve(summaryPathRaw) : buildDefaultSummaryPath(jsonlPath);
|
|
891
|
-
|
|
892
|
-
const { payload: snapshot, resolvedPath: snapshotPath } = readJsonFile(snapshotPathRaw);
|
|
893
|
-
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
894
|
-
if (!validation.ok) {
|
|
895
|
-
console.log(JSON.stringify({
|
|
896
|
-
ok: false,
|
|
897
|
-
file: sourcePath,
|
|
898
|
-
errors: validation.errors,
|
|
899
|
-
warnings: validation.warnings,
|
|
900
|
-
}, null, 2));
|
|
901
|
-
process.exitCode = 1;
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (fromNode && !script.operations.some((op) => op.id === fromNode)) {
|
|
905
|
-
throw new Error(`Unknown --from-node operation id: ${fromNode}`);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const profileId = profileOverride || snapshot?.profileId || script.profileId || getDefaultProfile();
|
|
909
|
-
if (!profileId) {
|
|
910
|
-
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
911
|
-
}
|
|
912
|
-
assertExistingProfile(profileId);
|
|
913
|
-
const resumeState = buildResumeStateFromSnapshot(script, snapshot, fromNode || null);
|
|
914
|
-
await executeAutoscriptRuntime({
|
|
915
|
-
commandName: 'autoscript.resume',
|
|
916
|
-
script,
|
|
917
|
-
sourcePath,
|
|
918
|
-
profileId,
|
|
919
|
-
jsonlPath,
|
|
920
|
-
summaryPath,
|
|
921
|
-
runnerOptions: {
|
|
922
|
-
initialState: resumeState.initialState,
|
|
923
|
-
forceRunOperationIds: resumeState.forceRunOperationIds,
|
|
924
|
-
},
|
|
925
|
-
extraStartPayload: {
|
|
926
|
-
snapshotPath,
|
|
927
|
-
fromNode: fromNode || null,
|
|
928
|
-
},
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
async function handleMockRun(args) {
|
|
933
|
-
const valueFlags = new Set(['--profile', '-p', '--fixture', '--jsonl-file', '--jsonl', '--summary-file', '--summary']);
|
|
934
|
-
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
935
|
-
if (!filePath) {
|
|
936
|
-
throw new Error('Usage: camo autoscript mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
937
|
-
}
|
|
938
|
-
const fixturePathRaw = readFlagValue(args, ['--fixture']);
|
|
939
|
-
if (!fixturePathRaw) {
|
|
940
|
-
throw new Error('autoscript mock-run requires --fixture <fixture.json>');
|
|
941
|
-
}
|
|
942
|
-
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
943
|
-
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
944
|
-
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
945
|
-
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
946
|
-
const summaryPath = summaryPathRaw ? path.resolve(summaryPathRaw) : buildDefaultSummaryPath(jsonlPath);
|
|
947
|
-
|
|
948
|
-
const { payload: fixture, resolvedPath: fixturePath } = readJsonFile(fixturePathRaw);
|
|
949
|
-
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
950
|
-
if (!validation.ok) {
|
|
951
|
-
console.log(JSON.stringify({
|
|
952
|
-
ok: false,
|
|
953
|
-
file: sourcePath,
|
|
954
|
-
errors: validation.errors,
|
|
955
|
-
warnings: validation.warnings,
|
|
956
|
-
}, null, 2));
|
|
957
|
-
process.exitCode = 1;
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
const profileId = profileOverride || fixture?.profileId || script.profileId || 'mock-profile';
|
|
961
|
-
await executeAutoscriptRuntime({
|
|
962
|
-
commandName: 'autoscript.mock_run',
|
|
963
|
-
script,
|
|
964
|
-
sourcePath,
|
|
965
|
-
profileId,
|
|
966
|
-
jsonlPath,
|
|
967
|
-
summaryPath,
|
|
968
|
-
runnerOptions: {
|
|
969
|
-
skipValidation: fixture?.skipValidation !== false,
|
|
970
|
-
mockEvents: Array.isArray(fixture?.events) ? fixture.events : [],
|
|
971
|
-
mockEventBaseDelayMs: Math.max(0, Number(fixture?.mockEventBaseDelayMs ?? 0) || 0),
|
|
972
|
-
stopWhenMockEventsExhausted: fixture?.stopWhenMockEventsExhausted !== false,
|
|
973
|
-
executeMockOperation: createMockOperationExecutor(fixture),
|
|
974
|
-
},
|
|
975
|
-
extraStartPayload: {
|
|
976
|
-
fixturePath,
|
|
977
|
-
},
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
export async function handleAutoscriptCommand(args) {
|
|
982
|
-
const sub = args[1];
|
|
983
|
-
switch (sub) {
|
|
984
|
-
case 'validate':
|
|
985
|
-
return handleValidate(args);
|
|
986
|
-
case 'explain':
|
|
987
|
-
return handleExplain(args);
|
|
988
|
-
case 'snapshot':
|
|
989
|
-
return handleSnapshot(args);
|
|
990
|
-
case 'replay':
|
|
991
|
-
return handleReplay(args);
|
|
992
|
-
case 'run':
|
|
993
|
-
return handleRun(args);
|
|
994
|
-
case 'resume':
|
|
995
|
-
return handleResume(args);
|
|
996
|
-
case 'mock-run':
|
|
997
|
-
return handleMockRun(args);
|
|
998
|
-
default:
|
|
999
|
-
console.log(`Usage: camo autoscript <validate|explain|snapshot|replay|run|resume|mock-run> [args]
|
|
1000
|
-
|
|
1001
|
-
Commands:
|
|
1002
|
-
validate <file> Validate autoscript schema and references
|
|
1003
|
-
explain <file> Print normalized graph and defaults
|
|
1004
|
-
snapshot <jsonl-file> [--out <snapshot-file>] Build resumable snapshot from run JSONL
|
|
1005
|
-
replay <jsonl-file> [--summary-file <path>] Rebuild summary from run JSONL
|
|
1006
|
-
run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run autoscript runtime
|
|
1007
|
-
resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Resume from snapshot
|
|
1008
|
-
mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run in mock replay mode
|
|
1009
|
-
`);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
|
|
4
|
+
import { explainAutoscript, loadAndValidateAutoscript } from '../autoscript/schema.mjs';
|
|
5
|
+
import { AutoscriptRunner } from '../autoscript/runtime.mjs';
|
|
6
|
+
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
7
|
+
|
|
8
|
+
function readFlagValue(args, names) {
|
|
9
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
10
|
+
if (!names.includes(args[i])) continue;
|
|
11
|
+
const value = args[i + 1];
|
|
12
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectPositionals(args, startIndex = 2, valueFlags = new Set(['--profile', '-p'])) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (let i = startIndex; i < args.length; i += 1) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (!arg) continue;
|
|
23
|
+
if (valueFlags.has(arg)) {
|
|
24
|
+
i += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (String(arg).startsWith('-')) continue;
|
|
28
|
+
out.push(arg);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertExistingProfile(profileId) {
|
|
34
|
+
const id = String(profileId || '').trim();
|
|
35
|
+
if (!id) {
|
|
36
|
+
throw new Error('profileId is required');
|
|
37
|
+
}
|
|
38
|
+
const known = new Set(listProfiles());
|
|
39
|
+
if (!known.has(id)) {
|
|
40
|
+
throw new Error(`profile not found: ${id}. create it first with "camo profile create ${id}"`);
|
|
41
|
+
}
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendJsonLine(filePath, payload) {
|
|
46
|
+
if (!filePath) return;
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
+
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeResultPayload(result) {
|
|
52
|
+
if (!result || typeof result !== 'object') return null;
|
|
53
|
+
if (result.result && typeof result.result === 'object' && !Array.isArray(result.result)) {
|
|
54
|
+
return result.result;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function bumpCounter(target, key) {
|
|
60
|
+
if (!target[key]) {
|
|
61
|
+
target[key] = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
target[key] += 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createRunSummaryTracker({ file, profileId, runId }) {
|
|
68
|
+
return {
|
|
69
|
+
file,
|
|
70
|
+
profileId,
|
|
71
|
+
runId,
|
|
72
|
+
runName: null,
|
|
73
|
+
startTs: null,
|
|
74
|
+
stopTs: null,
|
|
75
|
+
stopReason: null,
|
|
76
|
+
counts: {
|
|
77
|
+
operationStart: 0,
|
|
78
|
+
operationDone: 0,
|
|
79
|
+
operationError: 0,
|
|
80
|
+
operationSkipped: 0,
|
|
81
|
+
operationTerminal: 0,
|
|
82
|
+
watchError: 0,
|
|
83
|
+
},
|
|
84
|
+
target: {
|
|
85
|
+
visitedMax: 0,
|
|
86
|
+
notesClosed: 0,
|
|
87
|
+
harvestCount: 0,
|
|
88
|
+
likeOps: 0,
|
|
89
|
+
},
|
|
90
|
+
comments: {
|
|
91
|
+
total: 0,
|
|
92
|
+
expectedTotal: 0,
|
|
93
|
+
bottomReached: 0,
|
|
94
|
+
recoveriesTotal: 0,
|
|
95
|
+
coverageSum: 0,
|
|
96
|
+
coverageCount: 0,
|
|
97
|
+
exitReasonCounts: {},
|
|
98
|
+
commentsPathsCount: 0,
|
|
99
|
+
},
|
|
100
|
+
likes: {
|
|
101
|
+
scanned: 0,
|
|
102
|
+
hit: 0,
|
|
103
|
+
liked: 0,
|
|
104
|
+
dedupSkipped: 0,
|
|
105
|
+
alreadyLikedSkipped: 0,
|
|
106
|
+
verifyFailed: 0,
|
|
107
|
+
clickFailed: 0,
|
|
108
|
+
missingLikeControl: 0,
|
|
109
|
+
summaryCount: 0,
|
|
110
|
+
},
|
|
111
|
+
closeDetail: {
|
|
112
|
+
rollbackTotal: 0,
|
|
113
|
+
returnToSearchTotal: 0,
|
|
114
|
+
searchCountMax: 0,
|
|
115
|
+
pageExitReasonCounts: {},
|
|
116
|
+
},
|
|
117
|
+
terminal: {
|
|
118
|
+
operationId: null,
|
|
119
|
+
code: null,
|
|
120
|
+
ts: null,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyRunSummaryEvent(tracker, payload) {
|
|
126
|
+
if (!payload || typeof payload !== 'object') return;
|
|
127
|
+
const { event, ts } = payload;
|
|
128
|
+
if (event === 'autoscript:start') {
|
|
129
|
+
tracker.startTs = tracker.startTs || ts || null;
|
|
130
|
+
tracker.runId = tracker.runId || payload.runId || null;
|
|
131
|
+
tracker.profileId = tracker.profileId || payload.profileId || null;
|
|
132
|
+
tracker.runName = payload.name || tracker.runName;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (event === 'autoscript:stop') {
|
|
136
|
+
tracker.stopReason = payload.reason || tracker.stopReason;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (event === 'autoscript:watch_error') {
|
|
140
|
+
tracker.counts.watchError += 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (event === 'autoscript:operation_start') {
|
|
144
|
+
tracker.counts.operationStart += 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (event === 'autoscript:operation_error') {
|
|
148
|
+
tracker.counts.operationError += 1;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (event === 'autoscript:operation_skipped') {
|
|
152
|
+
tracker.counts.operationSkipped += 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (event === 'autoscript:operation_terminal') {
|
|
156
|
+
tracker.counts.operationTerminal += 1;
|
|
157
|
+
tracker.terminal.operationId = payload.operationId || tracker.terminal.operationId;
|
|
158
|
+
tracker.terminal.code = payload.code || tracker.terminal.code;
|
|
159
|
+
tracker.terminal.ts = payload.ts || tracker.terminal.ts;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (event !== 'autoscript:operation_done') return;
|
|
163
|
+
|
|
164
|
+
tracker.counts.operationDone += 1;
|
|
165
|
+
const result = normalizeResultPayload(payload.result);
|
|
166
|
+
const opId = payload.operationId;
|
|
167
|
+
if (!opId || !result || typeof result !== 'object') return;
|
|
168
|
+
|
|
169
|
+
if (opId === 'open_first_detail' || opId === 'open_next_detail') {
|
|
170
|
+
const visited = Number(result.visited);
|
|
171
|
+
if (Number.isFinite(visited)) {
|
|
172
|
+
tracker.target.visitedMax = Math.max(tracker.target.visitedMax, visited);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (opId === 'comments_harvest') {
|
|
178
|
+
tracker.target.harvestCount += 1;
|
|
179
|
+
tracker.comments.total += Number(result.collected || 0);
|
|
180
|
+
tracker.comments.expectedTotal += Number(result.expectedCommentsCount || 0);
|
|
181
|
+
tracker.comments.recoveriesTotal += Number(result.recoveries || 0);
|
|
182
|
+
if (result.reachedBottom === true) tracker.comments.bottomReached += 1;
|
|
183
|
+
if (result.commentsPath) tracker.comments.commentsPathsCount += 1;
|
|
184
|
+
const coverage = Number(result.commentCoverageRate);
|
|
185
|
+
if (Number.isFinite(coverage)) {
|
|
186
|
+
tracker.comments.coverageCount += 1;
|
|
187
|
+
tracker.comments.coverageSum += coverage;
|
|
188
|
+
}
|
|
189
|
+
bumpCounter(tracker.comments.exitReasonCounts, result.exitReason || 'unknown');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (opId === 'comment_like') {
|
|
194
|
+
tracker.target.likeOps += 1;
|
|
195
|
+
tracker.likes.scanned += Number(result.scannedCount || 0);
|
|
196
|
+
tracker.likes.hit += Number(result.hitCount || 0);
|
|
197
|
+
tracker.likes.liked += Number(result.likedCount || 0);
|
|
198
|
+
tracker.likes.dedupSkipped += Number(result.dedupSkipped || 0);
|
|
199
|
+
tracker.likes.alreadyLikedSkipped += Number(result.alreadyLikedSkipped || 0);
|
|
200
|
+
tracker.likes.verifyFailed += Number(result.verifyFailed || 0);
|
|
201
|
+
tracker.likes.clickFailed += Number(result.clickFailed || 0);
|
|
202
|
+
tracker.likes.missingLikeControl += Number(result.missingLikeControl || 0);
|
|
203
|
+
if (result.summaryPath) tracker.likes.summaryCount += 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (opId === 'close_detail') {
|
|
208
|
+
tracker.target.notesClosed += 1;
|
|
209
|
+
tracker.closeDetail.rollbackTotal += Number(result.rollbackCount || 0);
|
|
210
|
+
tracker.closeDetail.returnToSearchTotal += Number(result.returnToSearchCount || 0);
|
|
211
|
+
tracker.closeDetail.searchCountMax = Math.max(
|
|
212
|
+
tracker.closeDetail.searchCountMax,
|
|
213
|
+
Number(result.searchCount || 0),
|
|
214
|
+
);
|
|
215
|
+
bumpCounter(tracker.closeDetail.pageExitReasonCounts, result.pageExitReason || 'unknown');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildRunSummary(tracker, fallbackStopReason = null) {
|
|
220
|
+
const harvestCount = tracker.target.harvestCount;
|
|
221
|
+
const coverageCount = tracker.comments.coverageCount;
|
|
222
|
+
const durationSec = tracker.startTs && tracker.stopTs
|
|
223
|
+
? (new Date(tracker.stopTs).getTime() - new Date(tracker.startTs).getTime()) / 1000
|
|
224
|
+
: null;
|
|
225
|
+
return {
|
|
226
|
+
runId: tracker.runId || null,
|
|
227
|
+
profileId: tracker.profileId || null,
|
|
228
|
+
file: tracker.file || null,
|
|
229
|
+
runName: tracker.runName || null,
|
|
230
|
+
startTs: tracker.startTs || null,
|
|
231
|
+
stopTs: tracker.stopTs || null,
|
|
232
|
+
durationSec,
|
|
233
|
+
stopReason: tracker.stopReason || fallbackStopReason || null,
|
|
234
|
+
counts: tracker.counts,
|
|
235
|
+
target: tracker.target,
|
|
236
|
+
comments: {
|
|
237
|
+
total: tracker.comments.total,
|
|
238
|
+
expectedTotal: tracker.comments.expectedTotal,
|
|
239
|
+
avgPerNote: harvestCount > 0 ? tracker.comments.total / harvestCount : null,
|
|
240
|
+
bottomReached: tracker.comments.bottomReached,
|
|
241
|
+
bottomRate: harvestCount > 0 ? tracker.comments.bottomReached / harvestCount : null,
|
|
242
|
+
recoveriesTotal: tracker.comments.recoveriesTotal,
|
|
243
|
+
exitReasonCounts: tracker.comments.exitReasonCounts,
|
|
244
|
+
coverageAvg: coverageCount > 0 ? tracker.comments.coverageSum / coverageCount : null,
|
|
245
|
+
commentsPathsCount: tracker.comments.commentsPathsCount,
|
|
246
|
+
},
|
|
247
|
+
likes: tracker.likes,
|
|
248
|
+
closeDetail: tracker.closeDetail,
|
|
249
|
+
terminal: tracker.terminal.code
|
|
250
|
+
? tracker.terminal
|
|
251
|
+
: null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildDefaultSummaryPath(jsonlPath) {
|
|
256
|
+
if (!jsonlPath) return null;
|
|
257
|
+
return jsonlPath.endsWith('.jsonl')
|
|
258
|
+
? `${jsonlPath.slice(0, -'.jsonl'.length)}.summary.json`
|
|
259
|
+
: `${jsonlPath}.summary.json`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readJsonlEvents(filePath) {
|
|
263
|
+
const resolved = path.resolve(filePath);
|
|
264
|
+
if (!fs.existsSync(resolved)) {
|
|
265
|
+
throw new Error(`JSONL file not found: ${resolved}`);
|
|
266
|
+
}
|
|
267
|
+
const raw = fs.readFileSync(resolved, 'utf8');
|
|
268
|
+
const rows = raw
|
|
269
|
+
.split('\n')
|
|
270
|
+
.map((line) => line.trim())
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.map((line, index) => {
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(line);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw new Error(`Invalid JSONL at line ${index + 1}: ${err?.message || String(err)}`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return { resolvedPath: resolved, rows };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function cloneObject(value, fallback = {}) {
|
|
283
|
+
if (!value || typeof value !== 'object') return { ...fallback };
|
|
284
|
+
return { ...fallback, ...value };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function reduceSubscriptionState(subscriptionState, event) {
|
|
288
|
+
if (!event?.subscriptionId) return;
|
|
289
|
+
const prev = subscriptionState[event.subscriptionId] || {
|
|
290
|
+
exists: false,
|
|
291
|
+
appearCount: 0,
|
|
292
|
+
version: 0,
|
|
293
|
+
lastEventAt: null,
|
|
294
|
+
};
|
|
295
|
+
const next = { ...prev, lastEventAt: event.ts || null };
|
|
296
|
+
if (event.type === 'appear') {
|
|
297
|
+
next.exists = true;
|
|
298
|
+
next.appearCount = Number(prev.appearCount || 0) + 1;
|
|
299
|
+
next.version = Number(prev.version || 0) + 1;
|
|
300
|
+
} else if (event.type === 'exist') {
|
|
301
|
+
next.exists = true;
|
|
302
|
+
} else if (event.type === 'disappear') {
|
|
303
|
+
next.exists = false;
|
|
304
|
+
next.version = Number(prev.version || 0) + 1;
|
|
305
|
+
} else if (event.type === 'change') {
|
|
306
|
+
next.exists = Number(event.count || 0) > 0 || prev.exists === true;
|
|
307
|
+
next.version = Number(prev.version || 0) + 1;
|
|
308
|
+
}
|
|
309
|
+
subscriptionState[event.subscriptionId] = next;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildSnapshotFromEvents({
|
|
313
|
+
events,
|
|
314
|
+
file,
|
|
315
|
+
reason = 'log_snapshot',
|
|
316
|
+
}) {
|
|
317
|
+
const first = events[0] || null;
|
|
318
|
+
const last = events[events.length - 1] || null;
|
|
319
|
+
const tracker = createRunSummaryTracker({
|
|
320
|
+
file,
|
|
321
|
+
profileId: first?.profileId || null,
|
|
322
|
+
runId: first?.runId || null,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const operationState = {};
|
|
326
|
+
const subscriptionState = {};
|
|
327
|
+
let state = { active: false, reason: null, startedAt: null, stoppedAt: null };
|
|
328
|
+
let lastEvent = null;
|
|
329
|
+
|
|
330
|
+
for (const event of events) {
|
|
331
|
+
applyRunSummaryEvent(tracker, event);
|
|
332
|
+
|
|
333
|
+
if (event.event === 'autoscript:start') {
|
|
334
|
+
state = {
|
|
335
|
+
active: true,
|
|
336
|
+
reason: null,
|
|
337
|
+
startedAt: event.ts || null,
|
|
338
|
+
stoppedAt: null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (event.event === 'autoscript:event') {
|
|
343
|
+
reduceSubscriptionState(subscriptionState, event);
|
|
344
|
+
lastEvent = {
|
|
345
|
+
type: event.type || 'tick',
|
|
346
|
+
subscriptionId: event.subscriptionId || null,
|
|
347
|
+
selector: event.selector || null,
|
|
348
|
+
count: event.count ?? null,
|
|
349
|
+
timestamp: event.ts || null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (event.event === 'autoscript:operation_start') {
|
|
354
|
+
const prev = cloneObject(operationState[event.operationId], {
|
|
355
|
+
status: 'pending',
|
|
356
|
+
runs: 0,
|
|
357
|
+
lastError: null,
|
|
358
|
+
updatedAt: null,
|
|
359
|
+
result: null,
|
|
360
|
+
});
|
|
361
|
+
operationState[event.operationId] = {
|
|
362
|
+
...prev,
|
|
363
|
+
status: 'running',
|
|
364
|
+
updatedAt: event.ts || prev.updatedAt,
|
|
365
|
+
};
|
|
366
|
+
} else if (event.event === 'autoscript:operation_done') {
|
|
367
|
+
const prev = cloneObject(operationState[event.operationId], {
|
|
368
|
+
status: 'pending',
|
|
369
|
+
runs: 0,
|
|
370
|
+
lastError: null,
|
|
371
|
+
updatedAt: null,
|
|
372
|
+
result: null,
|
|
373
|
+
});
|
|
374
|
+
operationState[event.operationId] = {
|
|
375
|
+
...prev,
|
|
376
|
+
status: 'done',
|
|
377
|
+
runs: Number(prev.runs || 0) + 1,
|
|
378
|
+
lastError: null,
|
|
379
|
+
updatedAt: event.ts || prev.updatedAt,
|
|
380
|
+
result: event.result ?? null,
|
|
381
|
+
};
|
|
382
|
+
} else if (event.event === 'autoscript:operation_error') {
|
|
383
|
+
const prev = cloneObject(operationState[event.operationId], {
|
|
384
|
+
status: 'pending',
|
|
385
|
+
runs: 0,
|
|
386
|
+
lastError: null,
|
|
387
|
+
updatedAt: null,
|
|
388
|
+
result: null,
|
|
389
|
+
});
|
|
390
|
+
operationState[event.operationId] = {
|
|
391
|
+
...prev,
|
|
392
|
+
status: 'failed',
|
|
393
|
+
runs: Number(prev.runs || 0) + 1,
|
|
394
|
+
lastError: event.message || event.code || 'operation failed',
|
|
395
|
+
updatedAt: event.ts || prev.updatedAt,
|
|
396
|
+
result: null,
|
|
397
|
+
};
|
|
398
|
+
} else if (event.event === 'autoscript:operation_skipped') {
|
|
399
|
+
const prev = cloneObject(operationState[event.operationId], {
|
|
400
|
+
status: 'pending',
|
|
401
|
+
runs: 0,
|
|
402
|
+
lastError: null,
|
|
403
|
+
updatedAt: null,
|
|
404
|
+
result: null,
|
|
405
|
+
});
|
|
406
|
+
operationState[event.operationId] = {
|
|
407
|
+
...prev,
|
|
408
|
+
status: 'skipped',
|
|
409
|
+
runs: Number(prev.runs || 0) + 1,
|
|
410
|
+
lastError: null,
|
|
411
|
+
updatedAt: event.ts || prev.updatedAt,
|
|
412
|
+
result: {
|
|
413
|
+
code: event.code || null,
|
|
414
|
+
reason: event.reason || null,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
} else if (event.event === 'autoscript:operation_terminal') {
|
|
418
|
+
const prev = cloneObject(operationState[event.operationId], {
|
|
419
|
+
status: 'pending',
|
|
420
|
+
runs: 0,
|
|
421
|
+
lastError: null,
|
|
422
|
+
updatedAt: null,
|
|
423
|
+
result: null,
|
|
424
|
+
});
|
|
425
|
+
operationState[event.operationId] = {
|
|
426
|
+
...prev,
|
|
427
|
+
status: 'done',
|
|
428
|
+
runs: Number(prev.runs || 0) + 1,
|
|
429
|
+
lastError: null,
|
|
430
|
+
updatedAt: event.ts || prev.updatedAt,
|
|
431
|
+
result: {
|
|
432
|
+
terminalDoneCode: event.code || null,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
} else if (event.event === 'autoscript:stop') {
|
|
436
|
+
state = {
|
|
437
|
+
...state,
|
|
438
|
+
active: false,
|
|
439
|
+
reason: event.reason || state.reason || null,
|
|
440
|
+
stoppedAt: event.ts || state.stoppedAt || null,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
tracker.stopTs = last?.ts || tracker.stopTs || null;
|
|
446
|
+
const summary = buildRunSummary(tracker, state.reason || null);
|
|
447
|
+
const snapshot = {
|
|
448
|
+
kind: 'autoscript_snapshot',
|
|
449
|
+
version: 1,
|
|
450
|
+
reason,
|
|
451
|
+
createdAt: new Date().toISOString(),
|
|
452
|
+
sourceJsonl: file,
|
|
453
|
+
runId: summary.runId || null,
|
|
454
|
+
profileId: summary.profileId || null,
|
|
455
|
+
scriptName: summary.runName || null,
|
|
456
|
+
summary,
|
|
457
|
+
state: {
|
|
458
|
+
state,
|
|
459
|
+
subscriptionState,
|
|
460
|
+
operationState,
|
|
461
|
+
operationScheduleState: {},
|
|
462
|
+
runtimeContext: { vars: {}, tabPool: null, currentTab: null },
|
|
463
|
+
lastNavigationAt: 0,
|
|
464
|
+
lastEvent,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
return snapshot;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function toOperationOrder(script) {
|
|
471
|
+
return Array.isArray(script?.operations)
|
|
472
|
+
? script.operations.map((op) => String(op?.id || '').trim()).filter(Boolean)
|
|
473
|
+
: [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function buildDescendantSet(script, nodeId) {
|
|
477
|
+
const dependents = new Map();
|
|
478
|
+
for (const op of script.operations || []) {
|
|
479
|
+
for (const dep of op.dependsOn || []) {
|
|
480
|
+
if (!dependents.has(dep)) dependents.set(dep, new Set());
|
|
481
|
+
dependents.get(dep).add(op.id);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const seen = new Set([nodeId]);
|
|
485
|
+
const queue = [nodeId];
|
|
486
|
+
while (queue.length > 0) {
|
|
487
|
+
const current = queue.shift();
|
|
488
|
+
for (const next of dependents.get(current) || []) {
|
|
489
|
+
if (seen.has(next)) continue;
|
|
490
|
+
seen.add(next);
|
|
491
|
+
queue.push(next);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return seen;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function buildResumeStateFromSnapshot(script, snapshot, fromNodeId = null) {
|
|
498
|
+
const sourceState = snapshot?.state && typeof snapshot.state === 'object'
|
|
499
|
+
? snapshot.state
|
|
500
|
+
: {};
|
|
501
|
+
const sourceOps = sourceState.operationState && typeof sourceState.operationState === 'object'
|
|
502
|
+
? sourceState.operationState
|
|
503
|
+
: {};
|
|
504
|
+
const forceRunOperationIds = [];
|
|
505
|
+
const operationState = {};
|
|
506
|
+
const descendants = fromNodeId ? buildDescendantSet(script, fromNodeId) : null;
|
|
507
|
+
|
|
508
|
+
for (const opId of toOperationOrder(script)) {
|
|
509
|
+
const prev = sourceOps[opId] && typeof sourceOps[opId] === 'object'
|
|
510
|
+
? sourceOps[opId]
|
|
511
|
+
: {
|
|
512
|
+
status: 'pending',
|
|
513
|
+
runs: 0,
|
|
514
|
+
lastError: null,
|
|
515
|
+
updatedAt: null,
|
|
516
|
+
result: null,
|
|
517
|
+
};
|
|
518
|
+
if (!descendants) {
|
|
519
|
+
operationState[opId] = {
|
|
520
|
+
status: String(prev.status || 'pending'),
|
|
521
|
+
runs: Math.max(0, Number(prev.runs || 0) || 0),
|
|
522
|
+
lastError: prev.lastError || null,
|
|
523
|
+
updatedAt: prev.updatedAt || null,
|
|
524
|
+
result: prev.result ?? null,
|
|
525
|
+
};
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (descendants.has(opId)) {
|
|
530
|
+
operationState[opId] = {
|
|
531
|
+
status: 'pending',
|
|
532
|
+
runs: Math.max(0, Number(prev.runs || 0) || 0),
|
|
533
|
+
lastError: null,
|
|
534
|
+
updatedAt: prev.updatedAt || null,
|
|
535
|
+
result: null,
|
|
536
|
+
};
|
|
537
|
+
forceRunOperationIds.push(opId);
|
|
538
|
+
} else {
|
|
539
|
+
operationState[opId] = {
|
|
540
|
+
status: 'done',
|
|
541
|
+
runs: Math.max(1, Number(prev.runs || 1) || 1),
|
|
542
|
+
lastError: null,
|
|
543
|
+
updatedAt: prev.updatedAt || null,
|
|
544
|
+
result: prev.result ?? { code: 'RESUME_SKIPPED_PREVIOUS_BRANCH' },
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
initialState: {
|
|
551
|
+
state: {
|
|
552
|
+
state: sourceState.state || {
|
|
553
|
+
active: false,
|
|
554
|
+
reason: null,
|
|
555
|
+
startedAt: null,
|
|
556
|
+
stoppedAt: null,
|
|
557
|
+
},
|
|
558
|
+
subscriptionState: sourceState.subscriptionState || {},
|
|
559
|
+
operationState,
|
|
560
|
+
operationScheduleState: sourceState.operationScheduleState || {},
|
|
561
|
+
runtimeContext: sourceState.runtimeContext || { vars: {}, tabPool: null, currentTab: null },
|
|
562
|
+
lastNavigationAt: Number(sourceState.lastNavigationAt || 0) || 0,
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
forceRunOperationIds,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function createMockOperationExecutor(fixture) {
|
|
570
|
+
const source = fixture?.operations && typeof fixture.operations === 'object'
|
|
571
|
+
? fixture.operations
|
|
572
|
+
: {};
|
|
573
|
+
const queues = new Map(
|
|
574
|
+
Object.entries(source).map(([key, value]) => [key, Array.isArray(value) ? [...value] : [value]]),
|
|
575
|
+
);
|
|
576
|
+
const defaultResult = fixture?.defaultResult && typeof fixture.defaultResult === 'object'
|
|
577
|
+
? fixture.defaultResult
|
|
578
|
+
: { ok: true, code: 'OPERATION_DONE', message: 'mock operation done', data: { mock: true } };
|
|
579
|
+
|
|
580
|
+
return ({ operation }) => {
|
|
581
|
+
const key = String(operation?.id || '').trim();
|
|
582
|
+
const queue = queues.get(key);
|
|
583
|
+
const wildcard = queues.get('*');
|
|
584
|
+
const next = queue && queue.length > 0
|
|
585
|
+
? queue.shift()
|
|
586
|
+
: (wildcard && wildcard.length > 0 ? wildcard.shift() : defaultResult);
|
|
587
|
+
return next;
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function handleValidate(args) {
|
|
592
|
+
const filePath = collectPositionals(args)[0];
|
|
593
|
+
if (!filePath) {
|
|
594
|
+
throw new Error('Usage: camo autoscript validate <file>');
|
|
595
|
+
}
|
|
596
|
+
const { sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
597
|
+
console.log(JSON.stringify({
|
|
598
|
+
ok: validation.ok,
|
|
599
|
+
file: sourcePath,
|
|
600
|
+
errors: validation.errors,
|
|
601
|
+
warnings: validation.warnings,
|
|
602
|
+
operationOrder: validation.topologicalOrder,
|
|
603
|
+
}, null, 2));
|
|
604
|
+
if (!validation.ok) {
|
|
605
|
+
process.exitCode = 1;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function handleExplain(args) {
|
|
610
|
+
const filePath = collectPositionals(args)[0];
|
|
611
|
+
if (!filePath) {
|
|
612
|
+
throw new Error('Usage: camo autoscript explain <file>');
|
|
613
|
+
}
|
|
614
|
+
const { script, sourcePath } = loadAndValidateAutoscript(filePath);
|
|
615
|
+
const explained = explainAutoscript(script);
|
|
616
|
+
console.log(JSON.stringify({
|
|
617
|
+
ok: explained.ok,
|
|
618
|
+
file: sourcePath,
|
|
619
|
+
...explained,
|
|
620
|
+
}, null, 2));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function executeAutoscriptRuntime({
|
|
624
|
+
commandName,
|
|
625
|
+
script,
|
|
626
|
+
sourcePath,
|
|
627
|
+
profileId,
|
|
628
|
+
jsonlPath,
|
|
629
|
+
summaryPath,
|
|
630
|
+
runnerOptions = {},
|
|
631
|
+
extraStartPayload = {},
|
|
632
|
+
}) {
|
|
633
|
+
let jsonlWriteError = null;
|
|
634
|
+
const summaryTracker = createRunSummaryTracker({
|
|
635
|
+
file: sourcePath,
|
|
636
|
+
profileId,
|
|
637
|
+
runId: null,
|
|
638
|
+
});
|
|
639
|
+
const appendRunJsonl = (payload) => {
|
|
640
|
+
if (!jsonlPath) return;
|
|
641
|
+
if (jsonlWriteError) return;
|
|
642
|
+
try {
|
|
643
|
+
appendJsonLine(jsonlPath, payload);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
jsonlWriteError = err;
|
|
646
|
+
console.error(JSON.stringify({
|
|
647
|
+
event: 'autoscript:jsonl_error',
|
|
648
|
+
file: jsonlPath,
|
|
649
|
+
message: err?.message || String(err),
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const runner = new AutoscriptRunner({
|
|
655
|
+
...script,
|
|
656
|
+
profileId,
|
|
657
|
+
}, {
|
|
658
|
+
profileId,
|
|
659
|
+
...runnerOptions,
|
|
660
|
+
log: (payload) => {
|
|
661
|
+
console.log(JSON.stringify(payload));
|
|
662
|
+
appendRunJsonl(payload);
|
|
663
|
+
applyRunSummaryEvent(summaryTracker, payload);
|
|
664
|
+
safeAppendProgressEvent({
|
|
665
|
+
source: 'autoscript.runtime',
|
|
666
|
+
mode: 'autoscript',
|
|
667
|
+
profileId: payload.profileId || profileId,
|
|
668
|
+
runId: payload.runId || null,
|
|
669
|
+
event: payload.event || 'autoscript.log',
|
|
670
|
+
payload,
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const running = await runner.start();
|
|
676
|
+
summaryTracker.runId = running.runId;
|
|
677
|
+
safeAppendProgressEvent({
|
|
678
|
+
source: 'autoscript.command',
|
|
679
|
+
mode: 'autoscript',
|
|
680
|
+
profileId,
|
|
681
|
+
runId: running.runId,
|
|
682
|
+
event: `${commandName}.start`,
|
|
683
|
+
payload: {
|
|
684
|
+
file: sourcePath,
|
|
685
|
+
profileId,
|
|
686
|
+
runId: running.runId,
|
|
687
|
+
...extraStartPayload,
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
console.log(JSON.stringify({
|
|
691
|
+
ok: true,
|
|
692
|
+
command: commandName,
|
|
693
|
+
file: sourcePath,
|
|
694
|
+
profileId,
|
|
695
|
+
runId: running.runId,
|
|
696
|
+
message: 'Autoscript runtime started. Press Ctrl+C to stop.',
|
|
697
|
+
...extraStartPayload,
|
|
698
|
+
}, null, 2));
|
|
699
|
+
appendRunJsonl({
|
|
700
|
+
runId: running.runId,
|
|
701
|
+
profileId,
|
|
702
|
+
event: `${commandName}:run_start`,
|
|
703
|
+
ts: new Date().toISOString(),
|
|
704
|
+
file: sourcePath,
|
|
705
|
+
jsonlPath,
|
|
706
|
+
...extraStartPayload,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const onSigint = () => {
|
|
710
|
+
running.stop('signal_interrupt');
|
|
711
|
+
};
|
|
712
|
+
process.once('SIGINT', onSigint);
|
|
713
|
+
|
|
714
|
+
const done = await running.done.finally(() => {
|
|
715
|
+
process.removeListener('SIGINT', onSigint);
|
|
716
|
+
});
|
|
717
|
+
summaryTracker.stopTs = new Date().toISOString();
|
|
718
|
+
const summaryPayload = buildRunSummary(summaryTracker, done?.reason || null);
|
|
719
|
+
if (summaryPath) {
|
|
720
|
+
fs.mkdirSync(path.dirname(summaryPath), { recursive: true });
|
|
721
|
+
fs.writeFileSync(summaryPath, `${JSON.stringify(summaryPayload, null, 2)}\n`, 'utf8');
|
|
722
|
+
}
|
|
723
|
+
console.log(JSON.stringify({
|
|
724
|
+
event: 'autoscript:run_summary',
|
|
725
|
+
runId: running.runId,
|
|
726
|
+
profileId,
|
|
727
|
+
summaryPath,
|
|
728
|
+
summary: summaryPayload,
|
|
729
|
+
}, null, 2));
|
|
730
|
+
safeAppendProgressEvent({
|
|
731
|
+
source: 'autoscript.command',
|
|
732
|
+
mode: 'autoscript',
|
|
733
|
+
profileId,
|
|
734
|
+
runId: running.runId,
|
|
735
|
+
event: `${commandName}.stop`,
|
|
736
|
+
payload: {
|
|
737
|
+
file: sourcePath,
|
|
738
|
+
profileId,
|
|
739
|
+
runId: running.runId,
|
|
740
|
+
reason: done?.reason || null,
|
|
741
|
+
...extraStartPayload,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
appendRunJsonl({
|
|
745
|
+
runId: running.runId,
|
|
746
|
+
profileId,
|
|
747
|
+
event: `${commandName}:run_stop`,
|
|
748
|
+
ts: new Date().toISOString(),
|
|
749
|
+
file: sourcePath,
|
|
750
|
+
reason: done?.reason || null,
|
|
751
|
+
...extraStartPayload,
|
|
752
|
+
});
|
|
753
|
+
appendRunJsonl({
|
|
754
|
+
runId: running.runId,
|
|
755
|
+
profileId,
|
|
756
|
+
event: `${commandName}:run_summary`,
|
|
757
|
+
ts: new Date().toISOString(),
|
|
758
|
+
summaryPath,
|
|
759
|
+
summary: summaryPayload,
|
|
760
|
+
...extraStartPayload,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
done,
|
|
765
|
+
summaryPath,
|
|
766
|
+
summary: summaryPayload,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function handleRun(args) {
|
|
771
|
+
const filePath = collectPositionals(args)[0];
|
|
772
|
+
if (!filePath) {
|
|
773
|
+
throw new Error('Usage: camo autoscript run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
774
|
+
}
|
|
775
|
+
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
776
|
+
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
777
|
+
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
778
|
+
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
779
|
+
const summaryPath = summaryPathRaw
|
|
780
|
+
? path.resolve(summaryPathRaw)
|
|
781
|
+
: buildDefaultSummaryPath(jsonlPath);
|
|
782
|
+
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
783
|
+
if (!validation.ok) {
|
|
784
|
+
console.log(JSON.stringify({
|
|
785
|
+
ok: false,
|
|
786
|
+
file: sourcePath,
|
|
787
|
+
errors: validation.errors,
|
|
788
|
+
warnings: validation.warnings,
|
|
789
|
+
}, null, 2));
|
|
790
|
+
process.exitCode = 1;
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const profileId = profileOverride || script.profileId || getDefaultProfile();
|
|
795
|
+
if (!profileId) {
|
|
796
|
+
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
797
|
+
}
|
|
798
|
+
assertExistingProfile(profileId);
|
|
799
|
+
|
|
800
|
+
await executeAutoscriptRuntime({
|
|
801
|
+
commandName: 'autoscript.run',
|
|
802
|
+
script,
|
|
803
|
+
sourcePath,
|
|
804
|
+
profileId,
|
|
805
|
+
jsonlPath,
|
|
806
|
+
summaryPath,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function readJsonFile(filePath) {
|
|
811
|
+
const resolved = path.resolve(filePath);
|
|
812
|
+
if (!fs.existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
813
|
+
try {
|
|
814
|
+
return {
|
|
815
|
+
resolvedPath: resolved,
|
|
816
|
+
payload: JSON.parse(fs.readFileSync(resolved, 'utf8')),
|
|
817
|
+
};
|
|
818
|
+
} catch (err) {
|
|
819
|
+
throw new Error(`Invalid JSON file: ${resolved} (${err?.message || String(err)})`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function handleSnapshot(args) {
|
|
824
|
+
const valueFlags = new Set(['--out', '-o']);
|
|
825
|
+
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
826
|
+
if (!filePath) {
|
|
827
|
+
throw new Error('Usage: camo autoscript snapshot <jsonl-file> [--out <snapshot-file>]');
|
|
828
|
+
}
|
|
829
|
+
const outRaw = readFlagValue(args, ['--out', '-o']);
|
|
830
|
+
const { resolvedPath, rows } = readJsonlEvents(filePath);
|
|
831
|
+
const snapshot = buildSnapshotFromEvents({ events: rows, file: resolvedPath });
|
|
832
|
+
const outputPath = outRaw
|
|
833
|
+
? path.resolve(outRaw)
|
|
834
|
+
: (resolvedPath.endsWith('.jsonl')
|
|
835
|
+
? `${resolvedPath.slice(0, -'.jsonl'.length)}.snapshot.json`
|
|
836
|
+
: `${resolvedPath}.snapshot.json`);
|
|
837
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
838
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
839
|
+
console.log(JSON.stringify({
|
|
840
|
+
ok: true,
|
|
841
|
+
command: 'autoscript.snapshot',
|
|
842
|
+
input: resolvedPath,
|
|
843
|
+
output: outputPath,
|
|
844
|
+
runId: snapshot.runId || null,
|
|
845
|
+
profileId: snapshot.profileId || null,
|
|
846
|
+
summary: snapshot.summary,
|
|
847
|
+
}, null, 2));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function handleReplay(args) {
|
|
851
|
+
const valueFlags = new Set(['--summary-file', '--summary']);
|
|
852
|
+
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
853
|
+
if (!filePath) {
|
|
854
|
+
throw new Error('Usage: camo autoscript replay <jsonl-file> [--summary-file <path>]');
|
|
855
|
+
}
|
|
856
|
+
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
857
|
+
const { resolvedPath, rows } = readJsonlEvents(filePath);
|
|
858
|
+
const snapshot = buildSnapshotFromEvents({ events: rows, file: resolvedPath, reason: 'log_replay' });
|
|
859
|
+
const summaryPath = summaryPathRaw
|
|
860
|
+
? path.resolve(summaryPathRaw)
|
|
861
|
+
: buildDefaultSummaryPath(resolvedPath);
|
|
862
|
+
if (summaryPath) {
|
|
863
|
+
fs.mkdirSync(path.dirname(summaryPath), { recursive: true });
|
|
864
|
+
fs.writeFileSync(summaryPath, `${JSON.stringify(snapshot.summary, null, 2)}\n`, 'utf8');
|
|
865
|
+
}
|
|
866
|
+
console.log(JSON.stringify({
|
|
867
|
+
ok: true,
|
|
868
|
+
command: 'autoscript.replay',
|
|
869
|
+
input: resolvedPath,
|
|
870
|
+
summaryPath,
|
|
871
|
+
summary: snapshot.summary,
|
|
872
|
+
}, null, 2));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function handleResume(args) {
|
|
876
|
+
const valueFlags = new Set(['--profile', '-p', '--snapshot', '--from-node', '--jsonl-file', '--jsonl', '--summary-file', '--summary']);
|
|
877
|
+
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
878
|
+
if (!filePath) {
|
|
879
|
+
throw new Error('Usage: camo autoscript resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
880
|
+
}
|
|
881
|
+
const snapshotPathRaw = readFlagValue(args, ['--snapshot']);
|
|
882
|
+
if (!snapshotPathRaw) {
|
|
883
|
+
throw new Error('autoscript resume requires --snapshot <snapshot-file>');
|
|
884
|
+
}
|
|
885
|
+
const fromNode = readFlagValue(args, ['--from-node']);
|
|
886
|
+
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
887
|
+
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
888
|
+
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
889
|
+
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
890
|
+
const summaryPath = summaryPathRaw ? path.resolve(summaryPathRaw) : buildDefaultSummaryPath(jsonlPath);
|
|
891
|
+
|
|
892
|
+
const { payload: snapshot, resolvedPath: snapshotPath } = readJsonFile(snapshotPathRaw);
|
|
893
|
+
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
894
|
+
if (!validation.ok) {
|
|
895
|
+
console.log(JSON.stringify({
|
|
896
|
+
ok: false,
|
|
897
|
+
file: sourcePath,
|
|
898
|
+
errors: validation.errors,
|
|
899
|
+
warnings: validation.warnings,
|
|
900
|
+
}, null, 2));
|
|
901
|
+
process.exitCode = 1;
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (fromNode && !script.operations.some((op) => op.id === fromNode)) {
|
|
905
|
+
throw new Error(`Unknown --from-node operation id: ${fromNode}`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const profileId = profileOverride || snapshot?.profileId || script.profileId || getDefaultProfile();
|
|
909
|
+
if (!profileId) {
|
|
910
|
+
throw new Error('profileId is required. Set in script or pass --profile <id>');
|
|
911
|
+
}
|
|
912
|
+
assertExistingProfile(profileId);
|
|
913
|
+
const resumeState = buildResumeStateFromSnapshot(script, snapshot, fromNode || null);
|
|
914
|
+
await executeAutoscriptRuntime({
|
|
915
|
+
commandName: 'autoscript.resume',
|
|
916
|
+
script,
|
|
917
|
+
sourcePath,
|
|
918
|
+
profileId,
|
|
919
|
+
jsonlPath,
|
|
920
|
+
summaryPath,
|
|
921
|
+
runnerOptions: {
|
|
922
|
+
initialState: resumeState.initialState,
|
|
923
|
+
forceRunOperationIds: resumeState.forceRunOperationIds,
|
|
924
|
+
},
|
|
925
|
+
extraStartPayload: {
|
|
926
|
+
snapshotPath,
|
|
927
|
+
fromNode: fromNode || null,
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function handleMockRun(args) {
|
|
933
|
+
const valueFlags = new Set(['--profile', '-p', '--fixture', '--jsonl-file', '--jsonl', '--summary-file', '--summary']);
|
|
934
|
+
const filePath = collectPositionals(args, 2, valueFlags)[0];
|
|
935
|
+
if (!filePath) {
|
|
936
|
+
throw new Error('Usage: camo autoscript mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]');
|
|
937
|
+
}
|
|
938
|
+
const fixturePathRaw = readFlagValue(args, ['--fixture']);
|
|
939
|
+
if (!fixturePathRaw) {
|
|
940
|
+
throw new Error('autoscript mock-run requires --fixture <fixture.json>');
|
|
941
|
+
}
|
|
942
|
+
const profileOverride = readFlagValue(args, ['--profile', '-p']);
|
|
943
|
+
const jsonlPathRaw = readFlagValue(args, ['--jsonl-file', '--jsonl']);
|
|
944
|
+
const jsonlPath = jsonlPathRaw ? path.resolve(jsonlPathRaw) : null;
|
|
945
|
+
const summaryPathRaw = readFlagValue(args, ['--summary-file', '--summary']);
|
|
946
|
+
const summaryPath = summaryPathRaw ? path.resolve(summaryPathRaw) : buildDefaultSummaryPath(jsonlPath);
|
|
947
|
+
|
|
948
|
+
const { payload: fixture, resolvedPath: fixturePath } = readJsonFile(fixturePathRaw);
|
|
949
|
+
const { script, sourcePath, validation } = loadAndValidateAutoscript(filePath);
|
|
950
|
+
if (!validation.ok) {
|
|
951
|
+
console.log(JSON.stringify({
|
|
952
|
+
ok: false,
|
|
953
|
+
file: sourcePath,
|
|
954
|
+
errors: validation.errors,
|
|
955
|
+
warnings: validation.warnings,
|
|
956
|
+
}, null, 2));
|
|
957
|
+
process.exitCode = 1;
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const profileId = profileOverride || fixture?.profileId || script.profileId || 'mock-profile';
|
|
961
|
+
await executeAutoscriptRuntime({
|
|
962
|
+
commandName: 'autoscript.mock_run',
|
|
963
|
+
script,
|
|
964
|
+
sourcePath,
|
|
965
|
+
profileId,
|
|
966
|
+
jsonlPath,
|
|
967
|
+
summaryPath,
|
|
968
|
+
runnerOptions: {
|
|
969
|
+
skipValidation: fixture?.skipValidation !== false,
|
|
970
|
+
mockEvents: Array.isArray(fixture?.events) ? fixture.events : [],
|
|
971
|
+
mockEventBaseDelayMs: Math.max(0, Number(fixture?.mockEventBaseDelayMs ?? 0) || 0),
|
|
972
|
+
stopWhenMockEventsExhausted: fixture?.stopWhenMockEventsExhausted !== false,
|
|
973
|
+
executeMockOperation: createMockOperationExecutor(fixture),
|
|
974
|
+
},
|
|
975
|
+
extraStartPayload: {
|
|
976
|
+
fixturePath,
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export async function handleAutoscriptCommand(args) {
|
|
982
|
+
const sub = args[1];
|
|
983
|
+
switch (sub) {
|
|
984
|
+
case 'validate':
|
|
985
|
+
return handleValidate(args);
|
|
986
|
+
case 'explain':
|
|
987
|
+
return handleExplain(args);
|
|
988
|
+
case 'snapshot':
|
|
989
|
+
return handleSnapshot(args);
|
|
990
|
+
case 'replay':
|
|
991
|
+
return handleReplay(args);
|
|
992
|
+
case 'run':
|
|
993
|
+
return handleRun(args);
|
|
994
|
+
case 'resume':
|
|
995
|
+
return handleResume(args);
|
|
996
|
+
case 'mock-run':
|
|
997
|
+
return handleMockRun(args);
|
|
998
|
+
default:
|
|
999
|
+
console.log(`Usage: camo autoscript <validate|explain|snapshot|replay|run|resume|mock-run> [args]
|
|
1000
|
+
|
|
1001
|
+
Commands:
|
|
1002
|
+
validate <file> Validate autoscript schema and references
|
|
1003
|
+
explain <file> Print normalized graph and defaults
|
|
1004
|
+
snapshot <jsonl-file> [--out <snapshot-file>] Build resumable snapshot from run JSONL
|
|
1005
|
+
replay <jsonl-file> [--summary-file <path>] Rebuild summary from run JSONL
|
|
1006
|
+
run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run autoscript runtime
|
|
1007
|
+
resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Resume from snapshot
|
|
1008
|
+
mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run in mock replay mode
|
|
1009
|
+
`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|