@web-auto/camo 0.2.0 → 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 -1255
- 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 -127
- 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 -671
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -304
- 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 -222
- 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 -302
- 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
|
@@ -1,401 +1,401 @@
|
|
|
1
|
-
// Container commands - filter, watch, and subscription targets
|
|
2
|
-
import { getDomSnapshotByProfile, getSessionByProfile, getViewportByProfile } from '../utils/browser-service.mjs';
|
|
3
|
-
import { getDefaultProfile } from '../utils/config.mjs';
|
|
4
|
-
import { getChangeNotifier } from '../container/change-notifier.mjs';
|
|
5
|
-
import { createElementFilter } from '../container/element-filter.mjs';
|
|
6
|
-
import {
|
|
7
|
-
getRegisteredTargets,
|
|
8
|
-
initContainerSubscriptionDirectory,
|
|
9
|
-
listSubscriptionSets,
|
|
10
|
-
registerSubscriptionTargets,
|
|
11
|
-
} from '../container/subscription-registry.mjs';
|
|
12
|
-
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
13
|
-
|
|
14
|
-
const notifier = getChangeNotifier();
|
|
15
|
-
const elementFilter = createElementFilter();
|
|
16
|
-
|
|
17
|
-
const VALUE_FLAGS = new Set([
|
|
18
|
-
'--profile',
|
|
19
|
-
'-p',
|
|
20
|
-
'--selector',
|
|
21
|
-
'-s',
|
|
22
|
-
'--throttle',
|
|
23
|
-
'-t',
|
|
24
|
-
'--source',
|
|
25
|
-
'--site',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function readFlagValue(args, names) {
|
|
29
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
30
|
-
if (!names.includes(args[i])) continue;
|
|
31
|
-
const value = args[i + 1];
|
|
32
|
-
if (!value || String(value).startsWith('-')) return null;
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function collectPositionals(args, startIndex = 2) {
|
|
39
|
-
const out = [];
|
|
40
|
-
for (let i = startIndex; i < args.length; i += 1) {
|
|
41
|
-
const arg = args[i];
|
|
42
|
-
if (!arg) continue;
|
|
43
|
-
if (VALUE_FLAGS.has(arg)) {
|
|
44
|
-
i += 1;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (String(arg).startsWith('-')) continue;
|
|
48
|
-
out.push(arg);
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function ensureSession(profileId) {
|
|
54
|
-
const session = await getSessionByProfile(profileId);
|
|
55
|
-
if (!session) {
|
|
56
|
-
throw new Error(`No active session for profile: ${profileId || 'default'}`);
|
|
57
|
-
}
|
|
58
|
-
return session;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveWatchProfileAndSelectors(args) {
|
|
62
|
-
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
63
|
-
const explicitSelector = readFlagValue(args, ['--selector', '-s']);
|
|
64
|
-
const positionals = collectPositionals(args);
|
|
65
|
-
|
|
66
|
-
if (explicitSelector) {
|
|
67
|
-
const profileId = explicitProfile || positionals[0] || getDefaultProfile();
|
|
68
|
-
return { profileId, selectors: [explicitSelector], source: 'manual' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (explicitProfile) {
|
|
72
|
-
return {
|
|
73
|
-
profileId: explicitProfile,
|
|
74
|
-
selectors: positionals,
|
|
75
|
-
source: positionals.length > 0 ? 'manual' : 'subscription',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (positionals.length >= 2) {
|
|
80
|
-
return { profileId: positionals[0], selectors: positionals.slice(1), source: 'manual' };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (positionals.length === 1) {
|
|
84
|
-
const candidateProfile = getRegisteredTargets(positionals[0])?.profile;
|
|
85
|
-
if (candidateProfile) {
|
|
86
|
-
return { profileId: positionals[0], selectors: [], source: 'subscription' };
|
|
87
|
-
}
|
|
88
|
-
return { profileId: getDefaultProfile(), selectors: [positionals[0]], source: 'manual' };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return { profileId: getDefaultProfile(), selectors: [], source: 'subscription' };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveProfileAndSelectors(args) {
|
|
95
|
-
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
96
|
-
const positionals = collectPositionals(args);
|
|
97
|
-
if (explicitProfile) {
|
|
98
|
-
return { profileId: explicitProfile, selectors: positionals };
|
|
99
|
-
}
|
|
100
|
-
if (positionals.length >= 2) {
|
|
101
|
-
return { profileId: positionals[0], selectors: positionals.slice(1) };
|
|
102
|
-
}
|
|
103
|
-
return { profileId: getDefaultProfile(), selectors: positionals };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function resolveListProfile(args) {
|
|
107
|
-
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
108
|
-
if (explicitProfile) return explicitProfile;
|
|
109
|
-
const positionals = collectPositionals(args);
|
|
110
|
-
return positionals[0] || getDefaultProfile();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function handleContainerInitCommand(args) {
|
|
114
|
-
const source = readFlagValue(args, ['--source']);
|
|
115
|
-
const force = args.includes('--force');
|
|
116
|
-
const result = initContainerSubscriptionDirectory({
|
|
117
|
-
...(source ? { containerLibraryRoot: source } : {}),
|
|
118
|
-
force,
|
|
119
|
-
});
|
|
120
|
-
console.log(JSON.stringify(result, null, 2));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function handleContainerSetsCommand(args) {
|
|
124
|
-
const site = readFlagValue(args, ['--site']);
|
|
125
|
-
const result = listSubscriptionSets({ ...(site ? { site } : {}) });
|
|
126
|
-
console.log(JSON.stringify(result, null, 2));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function handleContainerRegisterCommand(args) {
|
|
130
|
-
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
131
|
-
const append = args.includes('--append');
|
|
132
|
-
const positionals = collectPositionals(args);
|
|
133
|
-
|
|
134
|
-
let profileId;
|
|
135
|
-
let setIds;
|
|
136
|
-
if (explicitProfile) {
|
|
137
|
-
profileId = explicitProfile;
|
|
138
|
-
setIds = positionals;
|
|
139
|
-
} else if (positionals.length >= 2) {
|
|
140
|
-
profileId = positionals[0];
|
|
141
|
-
setIds = positionals.slice(1);
|
|
142
|
-
} else {
|
|
143
|
-
profileId = getDefaultProfile();
|
|
144
|
-
setIds = positionals;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!profileId) {
|
|
148
|
-
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
149
|
-
}
|
|
150
|
-
if (!Array.isArray(setIds) || setIds.length === 0) {
|
|
151
|
-
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const result = registerSubscriptionTargets(profileId, setIds, { append });
|
|
155
|
-
console.log(JSON.stringify(result, null, 2));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function handleContainerTargetsCommand(args) {
|
|
159
|
-
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
160
|
-
const positionals = collectPositionals(args);
|
|
161
|
-
const profileId = explicitProfile || positionals[0] || null;
|
|
162
|
-
const result = getRegisteredTargets(profileId);
|
|
163
|
-
console.log(JSON.stringify(result, null, 2));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function handleContainerFilterCommand(args) {
|
|
167
|
-
const { profileId, selectors } = resolveProfileAndSelectors(args);
|
|
168
|
-
if (!profileId) {
|
|
169
|
-
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
170
|
-
}
|
|
171
|
-
if (selectors.length === 0) {
|
|
172
|
-
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const session = await ensureSession(profileId);
|
|
176
|
-
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
177
|
-
|
|
178
|
-
const matched = [];
|
|
179
|
-
for (const selector of selectors) {
|
|
180
|
-
const elements = notifier.findElements(snapshot, { css: selector });
|
|
181
|
-
matched.push(
|
|
182
|
-
...elements.map((element) => ({
|
|
183
|
-
selector,
|
|
184
|
-
path: element.path,
|
|
185
|
-
tag: element.tag,
|
|
186
|
-
id: element.id,
|
|
187
|
-
classes: element.classes,
|
|
188
|
-
text: (element.textSnippet || element.text || '').slice(0, 80),
|
|
189
|
-
})),
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
console.log(JSON.stringify({ ok: true, profileId: session.profileId || profileId, count: matched.length, elements: matched }, null, 2));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export async function handleContainerWatchCommand(args) {
|
|
197
|
-
const watchRequest = resolveWatchProfileAndSelectors(args);
|
|
198
|
-
const profileId = watchRequest.profileId;
|
|
199
|
-
if (!profileId) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
'Usage: camo container watch [profileId] [--selector <css>|<selector...>] [--throttle ms] [--profile <id>]',
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
let selectors = Array.from(
|
|
206
|
-
new Set(
|
|
207
|
-
(watchRequest.selectors || [])
|
|
208
|
-
.map((item) => String(item || '').trim())
|
|
209
|
-
.filter(Boolean),
|
|
210
|
-
),
|
|
211
|
-
);
|
|
212
|
-
let source = watchRequest.source;
|
|
213
|
-
|
|
214
|
-
if (selectors.length === 0) {
|
|
215
|
-
const registered = getRegisteredTargets(profileId)?.profile;
|
|
216
|
-
selectors = Array.from(
|
|
217
|
-
new Set(
|
|
218
|
-
(registered?.selectors || [])
|
|
219
|
-
.map((item) => item?.css)
|
|
220
|
-
.filter((css) => typeof css === 'string' && css.trim())
|
|
221
|
-
.map((css) => css.trim()),
|
|
222
|
-
),
|
|
223
|
-
);
|
|
224
|
-
source = 'subscription';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (selectors.length === 0) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`No selectors found for profile: ${profileId}. Use --selector <css> or run camo container register ${profileId} <setId...> first.`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const session = await ensureSession(profileId);
|
|
234
|
-
const throttleRaw = readFlagValue(args, ['--throttle', '-t']);
|
|
235
|
-
const throttle = Math.max(100, Number(throttleRaw) || 500);
|
|
236
|
-
|
|
237
|
-
console.log(JSON.stringify({
|
|
238
|
-
ok: true,
|
|
239
|
-
message: `Watching ${selectors.length} selector(s) from ${source}`,
|
|
240
|
-
selectors,
|
|
241
|
-
profileId: session.profileId || profileId,
|
|
242
|
-
throttle,
|
|
243
|
-
}));
|
|
244
|
-
safeAppendProgressEvent({
|
|
245
|
-
source: 'container.watch',
|
|
246
|
-
mode: 'normal',
|
|
247
|
-
profileId: session.profileId || profileId,
|
|
248
|
-
event: 'container.watch.start',
|
|
249
|
-
payload: {
|
|
250
|
-
selectors,
|
|
251
|
-
throttle,
|
|
252
|
-
source,
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const interval = setInterval(async () => {
|
|
257
|
-
try {
|
|
258
|
-
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
259
|
-
notifier.processSnapshot(snapshot);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
262
|
-
}
|
|
263
|
-
}, throttle);
|
|
264
|
-
|
|
265
|
-
const watchers = selectors.map((selector) => notifier.watch({ css: selector }, {
|
|
266
|
-
onAppear: (elements) => {
|
|
267
|
-
console.log(JSON.stringify({ event: 'appear', selector, count: elements.length, elements }));
|
|
268
|
-
safeAppendProgressEvent({
|
|
269
|
-
source: 'container.watch',
|
|
270
|
-
mode: 'normal',
|
|
271
|
-
profileId: session.profileId || profileId,
|
|
272
|
-
event: 'container.appear',
|
|
273
|
-
payload: { selector, count: elements.length },
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
onDisappear: (elements) => {
|
|
277
|
-
console.log(JSON.stringify({ event: 'disappear', selector, count: elements.length }));
|
|
278
|
-
safeAppendProgressEvent({
|
|
279
|
-
source: 'container.watch',
|
|
280
|
-
mode: 'normal',
|
|
281
|
-
profileId: session.profileId || profileId,
|
|
282
|
-
event: 'container.disappear',
|
|
283
|
-
payload: { selector, count: elements.length },
|
|
284
|
-
});
|
|
285
|
-
},
|
|
286
|
-
onChange: ({ appeared, disappeared }) => {
|
|
287
|
-
console.log(JSON.stringify({ event: 'change', selector, appeared: appeared.length, disappeared: disappeared.length }));
|
|
288
|
-
safeAppendProgressEvent({
|
|
289
|
-
source: 'container.watch',
|
|
290
|
-
mode: 'normal',
|
|
291
|
-
profileId: session.profileId || profileId,
|
|
292
|
-
event: 'container.change',
|
|
293
|
-
payload: { selector, appeared: appeared.length, disappeared: disappeared.length },
|
|
294
|
-
});
|
|
295
|
-
},
|
|
296
|
-
throttle,
|
|
297
|
-
}));
|
|
298
|
-
|
|
299
|
-
process.once('SIGINT', () => {
|
|
300
|
-
clearInterval(interval);
|
|
301
|
-
for (const stopWatch of watchers) stopWatch();
|
|
302
|
-
notifier.destroy();
|
|
303
|
-
console.log(JSON.stringify({ ok: true, message: 'Watch stopped' }));
|
|
304
|
-
safeAppendProgressEvent({
|
|
305
|
-
source: 'container.watch',
|
|
306
|
-
mode: 'normal',
|
|
307
|
-
profileId: session.profileId || profileId,
|
|
308
|
-
event: 'container.watch.stop',
|
|
309
|
-
payload: { selectors },
|
|
310
|
-
});
|
|
311
|
-
process.exit(0);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export async function handleContainerListCommand(args) {
|
|
316
|
-
const profileId = resolveListProfile(args);
|
|
317
|
-
if (!profileId) {
|
|
318
|
-
throw new Error('Usage: camo container list [profileId] [--profile <id>]');
|
|
319
|
-
}
|
|
320
|
-
const session = await ensureSession(profileId);
|
|
321
|
-
|
|
322
|
-
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
323
|
-
const viewport = await getViewportByProfile(session.profileId || profileId);
|
|
324
|
-
|
|
325
|
-
const collectElements = (node, domPath = 'root') => {
|
|
326
|
-
const elements = [];
|
|
327
|
-
if (!node) return elements;
|
|
328
|
-
|
|
329
|
-
const rect = node.rect || node.bbox;
|
|
330
|
-
if (rect && viewport) {
|
|
331
|
-
const inViewport = elementFilter.isInViewport(rect, viewport);
|
|
332
|
-
const visibilityRatio = elementFilter.getVisibilityRatio(rect, viewport);
|
|
333
|
-
if (inViewport && visibilityRatio > 0.1) {
|
|
334
|
-
elements.push({
|
|
335
|
-
path: domPath,
|
|
336
|
-
tag: node.tag,
|
|
337
|
-
id: node.id,
|
|
338
|
-
classes: node.classes?.slice(0, 3),
|
|
339
|
-
visibilityRatio: Math.round(visibilityRatio * 100) / 100,
|
|
340
|
-
rect: { x: rect.left || rect.x, y: rect.top || rect.y, w: rect.width, h: rect.height },
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (Array.isArray(node.children)) {
|
|
346
|
-
for (let i = 0; i < node.children.length; i += 1) {
|
|
347
|
-
elements.push(...collectElements(node.children[i], `${domPath}/${i}`));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return elements;
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const elements = collectElements(snapshot);
|
|
355
|
-
console.log(JSON.stringify({
|
|
356
|
-
ok: true,
|
|
357
|
-
profileId: session.profileId || profileId,
|
|
358
|
-
viewport,
|
|
359
|
-
count: elements.length,
|
|
360
|
-
elements: elements.slice(0, 50),
|
|
361
|
-
}, null, 2));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export async function handleContainerCommand(args) {
|
|
365
|
-
const sub = args[1];
|
|
366
|
-
|
|
367
|
-
switch (sub) {
|
|
368
|
-
case 'init':
|
|
369
|
-
return handleContainerInitCommand(args);
|
|
370
|
-
case 'sets':
|
|
371
|
-
return handleContainerSetsCommand(args);
|
|
372
|
-
case 'register':
|
|
373
|
-
return handleContainerRegisterCommand(args);
|
|
374
|
-
case 'targets':
|
|
375
|
-
return handleContainerTargetsCommand(args);
|
|
376
|
-
case 'filter':
|
|
377
|
-
return handleContainerFilterCommand(args);
|
|
378
|
-
case 'watch':
|
|
379
|
-
return handleContainerWatchCommand(args);
|
|
380
|
-
case 'list':
|
|
381
|
-
return handleContainerListCommand(args);
|
|
382
|
-
default:
|
|
383
|
-
console.log(`Usage: camo container <init|sets|register|targets|filter|watch|list> [options]
|
|
384
|
-
|
|
385
|
-
Commands:
|
|
386
|
-
init [--source <container-library-dir>] [--force] Initialize subscription directory and migrate container sets
|
|
387
|
-
sets [--site <siteKey>] List migrated subscription sets
|
|
388
|
-
register [profileId] <setId...> [--append] Register subscription targets for profile
|
|
389
|
-
targets [profileId] Show registered targets
|
|
390
|
-
filter [profileId] <selector...> Filter DOM elements by CSS selector
|
|
391
|
-
watch [profileId] [--selector <css>] [--throttle <ms>] Watch for element changes (defaults to registered selectors)
|
|
392
|
-
list [profileId] List visible elements in viewport
|
|
393
|
-
|
|
394
|
-
Options:
|
|
395
|
-
--profile, -p <id> Profile to use
|
|
396
|
-
--selector, -s <css> Selector for watch
|
|
397
|
-
--throttle, -t <ms> Poll interval for watch (default: 500)
|
|
398
|
-
--site <siteKey> Filter set list by site
|
|
399
|
-
`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
1
|
+
// Container commands - filter, watch, and subscription targets
|
|
2
|
+
import { getDomSnapshotByProfile, getSessionByProfile, getViewportByProfile } from '../utils/browser-service.mjs';
|
|
3
|
+
import { getDefaultProfile } from '../utils/config.mjs';
|
|
4
|
+
import { getChangeNotifier } from '../container/change-notifier.mjs';
|
|
5
|
+
import { createElementFilter } from '../container/element-filter.mjs';
|
|
6
|
+
import {
|
|
7
|
+
getRegisteredTargets,
|
|
8
|
+
initContainerSubscriptionDirectory,
|
|
9
|
+
listSubscriptionSets,
|
|
10
|
+
registerSubscriptionTargets,
|
|
11
|
+
} from '../container/subscription-registry.mjs';
|
|
12
|
+
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
13
|
+
|
|
14
|
+
const notifier = getChangeNotifier();
|
|
15
|
+
const elementFilter = createElementFilter();
|
|
16
|
+
|
|
17
|
+
const VALUE_FLAGS = new Set([
|
|
18
|
+
'--profile',
|
|
19
|
+
'-p',
|
|
20
|
+
'--selector',
|
|
21
|
+
'-s',
|
|
22
|
+
'--throttle',
|
|
23
|
+
'-t',
|
|
24
|
+
'--source',
|
|
25
|
+
'--site',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function readFlagValue(args, names) {
|
|
29
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
30
|
+
if (!names.includes(args[i])) continue;
|
|
31
|
+
const value = args[i + 1];
|
|
32
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectPositionals(args, startIndex = 2) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = startIndex; i < args.length; i += 1) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (!arg) continue;
|
|
43
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (String(arg).startsWith('-')) continue;
|
|
48
|
+
out.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureSession(profileId) {
|
|
54
|
+
const session = await getSessionByProfile(profileId);
|
|
55
|
+
if (!session) {
|
|
56
|
+
throw new Error(`No active session for profile: ${profileId || 'default'}`);
|
|
57
|
+
}
|
|
58
|
+
return session;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveWatchProfileAndSelectors(args) {
|
|
62
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
63
|
+
const explicitSelector = readFlagValue(args, ['--selector', '-s']);
|
|
64
|
+
const positionals = collectPositionals(args);
|
|
65
|
+
|
|
66
|
+
if (explicitSelector) {
|
|
67
|
+
const profileId = explicitProfile || positionals[0] || getDefaultProfile();
|
|
68
|
+
return { profileId, selectors: [explicitSelector], source: 'manual' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (explicitProfile) {
|
|
72
|
+
return {
|
|
73
|
+
profileId: explicitProfile,
|
|
74
|
+
selectors: positionals,
|
|
75
|
+
source: positionals.length > 0 ? 'manual' : 'subscription',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (positionals.length >= 2) {
|
|
80
|
+
return { profileId: positionals[0], selectors: positionals.slice(1), source: 'manual' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (positionals.length === 1) {
|
|
84
|
+
const candidateProfile = getRegisteredTargets(positionals[0])?.profile;
|
|
85
|
+
if (candidateProfile) {
|
|
86
|
+
return { profileId: positionals[0], selectors: [], source: 'subscription' };
|
|
87
|
+
}
|
|
88
|
+
return { profileId: getDefaultProfile(), selectors: [positionals[0]], source: 'manual' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { profileId: getDefaultProfile(), selectors: [], source: 'subscription' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProfileAndSelectors(args) {
|
|
95
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
96
|
+
const positionals = collectPositionals(args);
|
|
97
|
+
if (explicitProfile) {
|
|
98
|
+
return { profileId: explicitProfile, selectors: positionals };
|
|
99
|
+
}
|
|
100
|
+
if (positionals.length >= 2) {
|
|
101
|
+
return { profileId: positionals[0], selectors: positionals.slice(1) };
|
|
102
|
+
}
|
|
103
|
+
return { profileId: getDefaultProfile(), selectors: positionals };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveListProfile(args) {
|
|
107
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
108
|
+
if (explicitProfile) return explicitProfile;
|
|
109
|
+
const positionals = collectPositionals(args);
|
|
110
|
+
return positionals[0] || getDefaultProfile();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function handleContainerInitCommand(args) {
|
|
114
|
+
const source = readFlagValue(args, ['--source']);
|
|
115
|
+
const force = args.includes('--force');
|
|
116
|
+
const result = initContainerSubscriptionDirectory({
|
|
117
|
+
...(source ? { containerLibraryRoot: source } : {}),
|
|
118
|
+
force,
|
|
119
|
+
});
|
|
120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function handleContainerSetsCommand(args) {
|
|
124
|
+
const site = readFlagValue(args, ['--site']);
|
|
125
|
+
const result = listSubscriptionSets({ ...(site ? { site } : {}) });
|
|
126
|
+
console.log(JSON.stringify(result, null, 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function handleContainerRegisterCommand(args) {
|
|
130
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
131
|
+
const append = args.includes('--append');
|
|
132
|
+
const positionals = collectPositionals(args);
|
|
133
|
+
|
|
134
|
+
let profileId;
|
|
135
|
+
let setIds;
|
|
136
|
+
if (explicitProfile) {
|
|
137
|
+
profileId = explicitProfile;
|
|
138
|
+
setIds = positionals;
|
|
139
|
+
} else if (positionals.length >= 2) {
|
|
140
|
+
profileId = positionals[0];
|
|
141
|
+
setIds = positionals.slice(1);
|
|
142
|
+
} else {
|
|
143
|
+
profileId = getDefaultProfile();
|
|
144
|
+
setIds = positionals;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!profileId) {
|
|
148
|
+
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(setIds) || setIds.length === 0) {
|
|
151
|
+
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = registerSubscriptionTargets(profileId, setIds, { append });
|
|
155
|
+
console.log(JSON.stringify(result, null, 2));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function handleContainerTargetsCommand(args) {
|
|
159
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
160
|
+
const positionals = collectPositionals(args);
|
|
161
|
+
const profileId = explicitProfile || positionals[0] || null;
|
|
162
|
+
const result = getRegisteredTargets(profileId);
|
|
163
|
+
console.log(JSON.stringify(result, null, 2));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function handleContainerFilterCommand(args) {
|
|
167
|
+
const { profileId, selectors } = resolveProfileAndSelectors(args);
|
|
168
|
+
if (!profileId) {
|
|
169
|
+
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
170
|
+
}
|
|
171
|
+
if (selectors.length === 0) {
|
|
172
|
+
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const session = await ensureSession(profileId);
|
|
176
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
177
|
+
|
|
178
|
+
const matched = [];
|
|
179
|
+
for (const selector of selectors) {
|
|
180
|
+
const elements = notifier.findElements(snapshot, { css: selector });
|
|
181
|
+
matched.push(
|
|
182
|
+
...elements.map((element) => ({
|
|
183
|
+
selector,
|
|
184
|
+
path: element.path,
|
|
185
|
+
tag: element.tag,
|
|
186
|
+
id: element.id,
|
|
187
|
+
classes: element.classes,
|
|
188
|
+
text: (element.textSnippet || element.text || '').slice(0, 80),
|
|
189
|
+
})),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(JSON.stringify({ ok: true, profileId: session.profileId || profileId, count: matched.length, elements: matched }, null, 2));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleContainerWatchCommand(args) {
|
|
197
|
+
const watchRequest = resolveWatchProfileAndSelectors(args);
|
|
198
|
+
const profileId = watchRequest.profileId;
|
|
199
|
+
if (!profileId) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'Usage: camo container watch [profileId] [--selector <css>|<selector...>] [--throttle ms] [--profile <id>]',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let selectors = Array.from(
|
|
206
|
+
new Set(
|
|
207
|
+
(watchRequest.selectors || [])
|
|
208
|
+
.map((item) => String(item || '').trim())
|
|
209
|
+
.filter(Boolean),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
let source = watchRequest.source;
|
|
213
|
+
|
|
214
|
+
if (selectors.length === 0) {
|
|
215
|
+
const registered = getRegisteredTargets(profileId)?.profile;
|
|
216
|
+
selectors = Array.from(
|
|
217
|
+
new Set(
|
|
218
|
+
(registered?.selectors || [])
|
|
219
|
+
.map((item) => item?.css)
|
|
220
|
+
.filter((css) => typeof css === 'string' && css.trim())
|
|
221
|
+
.map((css) => css.trim()),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
source = 'subscription';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (selectors.length === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`No selectors found for profile: ${profileId}. Use --selector <css> or run camo container register ${profileId} <setId...> first.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const session = await ensureSession(profileId);
|
|
234
|
+
const throttleRaw = readFlagValue(args, ['--throttle', '-t']);
|
|
235
|
+
const throttle = Math.max(100, Number(throttleRaw) || 500);
|
|
236
|
+
|
|
237
|
+
console.log(JSON.stringify({
|
|
238
|
+
ok: true,
|
|
239
|
+
message: `Watching ${selectors.length} selector(s) from ${source}`,
|
|
240
|
+
selectors,
|
|
241
|
+
profileId: session.profileId || profileId,
|
|
242
|
+
throttle,
|
|
243
|
+
}));
|
|
244
|
+
safeAppendProgressEvent({
|
|
245
|
+
source: 'container.watch',
|
|
246
|
+
mode: 'normal',
|
|
247
|
+
profileId: session.profileId || profileId,
|
|
248
|
+
event: 'container.watch.start',
|
|
249
|
+
payload: {
|
|
250
|
+
selectors,
|
|
251
|
+
throttle,
|
|
252
|
+
source,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const interval = setInterval(async () => {
|
|
257
|
+
try {
|
|
258
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
259
|
+
notifier.processSnapshot(snapshot);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
262
|
+
}
|
|
263
|
+
}, throttle);
|
|
264
|
+
|
|
265
|
+
const watchers = selectors.map((selector) => notifier.watch({ css: selector }, {
|
|
266
|
+
onAppear: (elements) => {
|
|
267
|
+
console.log(JSON.stringify({ event: 'appear', selector, count: elements.length, elements }));
|
|
268
|
+
safeAppendProgressEvent({
|
|
269
|
+
source: 'container.watch',
|
|
270
|
+
mode: 'normal',
|
|
271
|
+
profileId: session.profileId || profileId,
|
|
272
|
+
event: 'container.appear',
|
|
273
|
+
payload: { selector, count: elements.length },
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
onDisappear: (elements) => {
|
|
277
|
+
console.log(JSON.stringify({ event: 'disappear', selector, count: elements.length }));
|
|
278
|
+
safeAppendProgressEvent({
|
|
279
|
+
source: 'container.watch',
|
|
280
|
+
mode: 'normal',
|
|
281
|
+
profileId: session.profileId || profileId,
|
|
282
|
+
event: 'container.disappear',
|
|
283
|
+
payload: { selector, count: elements.length },
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
onChange: ({ appeared, disappeared }) => {
|
|
287
|
+
console.log(JSON.stringify({ event: 'change', selector, appeared: appeared.length, disappeared: disappeared.length }));
|
|
288
|
+
safeAppendProgressEvent({
|
|
289
|
+
source: 'container.watch',
|
|
290
|
+
mode: 'normal',
|
|
291
|
+
profileId: session.profileId || profileId,
|
|
292
|
+
event: 'container.change',
|
|
293
|
+
payload: { selector, appeared: appeared.length, disappeared: disappeared.length },
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
throttle,
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
process.once('SIGINT', () => {
|
|
300
|
+
clearInterval(interval);
|
|
301
|
+
for (const stopWatch of watchers) stopWatch();
|
|
302
|
+
notifier.destroy();
|
|
303
|
+
console.log(JSON.stringify({ ok: true, message: 'Watch stopped' }));
|
|
304
|
+
safeAppendProgressEvent({
|
|
305
|
+
source: 'container.watch',
|
|
306
|
+
mode: 'normal',
|
|
307
|
+
profileId: session.profileId || profileId,
|
|
308
|
+
event: 'container.watch.stop',
|
|
309
|
+
payload: { selectors },
|
|
310
|
+
});
|
|
311
|
+
process.exit(0);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function handleContainerListCommand(args) {
|
|
316
|
+
const profileId = resolveListProfile(args);
|
|
317
|
+
if (!profileId) {
|
|
318
|
+
throw new Error('Usage: camo container list [profileId] [--profile <id>]');
|
|
319
|
+
}
|
|
320
|
+
const session = await ensureSession(profileId);
|
|
321
|
+
|
|
322
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
323
|
+
const viewport = await getViewportByProfile(session.profileId || profileId);
|
|
324
|
+
|
|
325
|
+
const collectElements = (node, domPath = 'root') => {
|
|
326
|
+
const elements = [];
|
|
327
|
+
if (!node) return elements;
|
|
328
|
+
|
|
329
|
+
const rect = node.rect || node.bbox;
|
|
330
|
+
if (rect && viewport) {
|
|
331
|
+
const inViewport = elementFilter.isInViewport(rect, viewport);
|
|
332
|
+
const visibilityRatio = elementFilter.getVisibilityRatio(rect, viewport);
|
|
333
|
+
if (inViewport && visibilityRatio > 0.1) {
|
|
334
|
+
elements.push({
|
|
335
|
+
path: domPath,
|
|
336
|
+
tag: node.tag,
|
|
337
|
+
id: node.id,
|
|
338
|
+
classes: node.classes?.slice(0, 3),
|
|
339
|
+
visibilityRatio: Math.round(visibilityRatio * 100) / 100,
|
|
340
|
+
rect: { x: rect.left || rect.x, y: rect.top || rect.y, w: rect.width, h: rect.height },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (Array.isArray(node.children)) {
|
|
346
|
+
for (let i = 0; i < node.children.length; i += 1) {
|
|
347
|
+
elements.push(...collectElements(node.children[i], `${domPath}/${i}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return elements;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const elements = collectElements(snapshot);
|
|
355
|
+
console.log(JSON.stringify({
|
|
356
|
+
ok: true,
|
|
357
|
+
profileId: session.profileId || profileId,
|
|
358
|
+
viewport,
|
|
359
|
+
count: elements.length,
|
|
360
|
+
elements: elements.slice(0, 50),
|
|
361
|
+
}, null, 2));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function handleContainerCommand(args) {
|
|
365
|
+
const sub = args[1];
|
|
366
|
+
|
|
367
|
+
switch (sub) {
|
|
368
|
+
case 'init':
|
|
369
|
+
return handleContainerInitCommand(args);
|
|
370
|
+
case 'sets':
|
|
371
|
+
return handleContainerSetsCommand(args);
|
|
372
|
+
case 'register':
|
|
373
|
+
return handleContainerRegisterCommand(args);
|
|
374
|
+
case 'targets':
|
|
375
|
+
return handleContainerTargetsCommand(args);
|
|
376
|
+
case 'filter':
|
|
377
|
+
return handleContainerFilterCommand(args);
|
|
378
|
+
case 'watch':
|
|
379
|
+
return handleContainerWatchCommand(args);
|
|
380
|
+
case 'list':
|
|
381
|
+
return handleContainerListCommand(args);
|
|
382
|
+
default:
|
|
383
|
+
console.log(`Usage: camo container <init|sets|register|targets|filter|watch|list> [options]
|
|
384
|
+
|
|
385
|
+
Commands:
|
|
386
|
+
init [--source <container-library-dir>] [--force] Initialize subscription directory and migrate container sets
|
|
387
|
+
sets [--site <siteKey>] List migrated subscription sets
|
|
388
|
+
register [profileId] <setId...> [--append] Register subscription targets for profile
|
|
389
|
+
targets [profileId] Show registered targets
|
|
390
|
+
filter [profileId] <selector...> Filter DOM elements by CSS selector
|
|
391
|
+
watch [profileId] [--selector <css>] [--throttle <ms>] Watch for element changes (defaults to registered selectors)
|
|
392
|
+
list [profileId] List visible elements in viewport
|
|
393
|
+
|
|
394
|
+
Options:
|
|
395
|
+
--profile, -p <id> Profile to use
|
|
396
|
+
--selector, -s <css> Selector for watch
|
|
397
|
+
--throttle, -t <ms> Poll interval for watch (default: 500)
|
|
398
|
+
--site <siteKey> Filter set list by site
|
|
399
|
+
`);
|
|
400
|
+
}
|
|
401
|
+
}
|