@web-auto/camo 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/cli.mjs +14 -1
- package/src/commands/devtools.mjs +349 -0
- package/src/utils/help.mjs +7 -0
package/README.md
CHANGED
|
@@ -42,6 +42,12 @@ camo start worker-1 --headless --alias shard1 --idle-timeout 30m
|
|
|
42
42
|
# Start with devtools (headful only)
|
|
43
43
|
camo start worker-1 --devtools
|
|
44
44
|
|
|
45
|
+
# Evaluate JS (devtools-style input in page context)
|
|
46
|
+
camo devtools eval worker-1 "document.title"
|
|
47
|
+
|
|
48
|
+
# Read captured console entries
|
|
49
|
+
camo devtools logs worker-1 --levels error,warn --limit 50
|
|
50
|
+
|
|
45
51
|
# Navigate
|
|
46
52
|
camo goto https://www.xiaohongshu.com
|
|
47
53
|
|
|
@@ -122,6 +128,14 @@ camo clear-highlight [profileId] # Clear all highlights
|
|
|
122
128
|
camo viewport [profileId] --width <w> --height <h>
|
|
123
129
|
```
|
|
124
130
|
|
|
131
|
+
### Devtools
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
camo devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
135
|
+
camo devtools eval [profileId] <expression> [--profile <id>]
|
|
136
|
+
camo devtools clear [profileId]
|
|
137
|
+
```
|
|
138
|
+
|
|
125
139
|
### Pages
|
|
126
140
|
|
|
127
141
|
```bash
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { handleSystemCommand } from './commands/system.mjs';
|
|
|
13
13
|
import { handleContainerCommand } from './commands/container.mjs';
|
|
14
14
|
import { handleAutoscriptCommand } from './commands/autoscript.mjs';
|
|
15
15
|
import { handleEventsCommand } from './commands/events.mjs';
|
|
16
|
+
import { handleDevtoolsCommand } from './commands/devtools.mjs';
|
|
16
17
|
import {
|
|
17
18
|
handleStartCommand, handleStopCommand, handleStatusCommand,
|
|
18
19
|
handleGotoCommand, handleBackCommand, handleScreenshotCommand,
|
|
@@ -64,6 +65,13 @@ function inferProfileId(cmd, args) {
|
|
|
64
65
|
return positionals[0] || null;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
if (cmd === 'devtools') {
|
|
69
|
+
const sub = positionals[0] || null;
|
|
70
|
+
if (sub === 'eval' || sub === 'logs' || sub === 'clear') {
|
|
71
|
+
return positionals[1] || null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
if (cmd === 'autoscript' && positionals[0] === 'run') {
|
|
68
76
|
return explicitProfile || null;
|
|
69
77
|
}
|
|
@@ -192,6 +200,11 @@ async function main() {
|
|
|
192
200
|
return;
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
if (cmd === 'devtools') {
|
|
204
|
+
await runTrackedCommand(cmd, args, () => handleDevtoolsCommand(args));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
195
208
|
// Lifecycle commands
|
|
196
209
|
if (cmd === 'cleanup') {
|
|
197
210
|
await runTrackedCommand(cmd, args, () => handleCleanupCommand(args));
|
|
@@ -237,7 +250,7 @@ async function main() {
|
|
|
237
250
|
'start', 'stop', 'close', 'status', 'list', 'goto', 'navigate', 'back', 'screenshot',
|
|
238
251
|
'new-page', 'close-page', 'switch-page', 'list-pages', 'shutdown',
|
|
239
252
|
'scroll', 'click', 'type', 'highlight', 'clear-highlight', 'viewport',
|
|
240
|
-
'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events',
|
|
253
|
+
'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events', 'devtools',
|
|
241
254
|
]);
|
|
242
255
|
|
|
243
256
|
if (!serviceCommands.has(cmd)) {
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
|
|
2
|
+
import { callAPI } from '../utils/browser-service.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LIMIT = 120;
|
|
5
|
+
const MAX_LIMIT = 1000;
|
|
6
|
+
|
|
7
|
+
function parseNumber(value, fallback) {
|
|
8
|
+
const num = Number(value);
|
|
9
|
+
return Number.isFinite(num) ? num : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clamp(value, min, max) {
|
|
13
|
+
return Math.min(Math.max(value, min), max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readFlagValue(args, names) {
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
if (!names.includes(args[i])) continue;
|
|
19
|
+
const value = args[i + 1];
|
|
20
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectPositionals(args, startIndex = 2) {
|
|
27
|
+
const values = [];
|
|
28
|
+
for (let i = startIndex; i < args.length; i += 1) {
|
|
29
|
+
const token = args[i];
|
|
30
|
+
if (!token || String(token).startsWith('--')) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const prev = args[i - 1];
|
|
34
|
+
if (prev && ['--profile', '-p', '--limit', '-n', '--levels', '--since'].includes(prev)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
values.push(String(token));
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pickProfileAndExpression(args, subcommand) {
|
|
43
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
44
|
+
const profileSet = new Set(listProfiles());
|
|
45
|
+
const positionals = collectPositionals(args, 2);
|
|
46
|
+
|
|
47
|
+
let profileId = explicitProfile || null;
|
|
48
|
+
let expression = null;
|
|
49
|
+
|
|
50
|
+
if (subcommand === 'eval') {
|
|
51
|
+
if (positionals.length === 0) {
|
|
52
|
+
return { profileId: profileId || getDefaultProfile(), expression: null };
|
|
53
|
+
}
|
|
54
|
+
if (!profileId && positionals.length >= 2) {
|
|
55
|
+
profileId = positionals[0];
|
|
56
|
+
expression = positionals.slice(1).join(' ').trim();
|
|
57
|
+
} else if (!profileId && profileSet.has(positionals[0])) {
|
|
58
|
+
profileId = positionals[0];
|
|
59
|
+
expression = positionals.slice(1).join(' ').trim();
|
|
60
|
+
} else {
|
|
61
|
+
expression = positionals.join(' ').trim();
|
|
62
|
+
}
|
|
63
|
+
return { profileId: profileId || getDefaultProfile(), expression };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (positionals.length > 0) {
|
|
67
|
+
if (!profileId && (profileSet.has(positionals[0]) || subcommand === 'logs' || subcommand === 'clear')) {
|
|
68
|
+
profileId = positionals[0];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { profileId: profileId || getDefaultProfile(), expression: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildConsoleInstallScript(maxEntries) {
|
|
75
|
+
return `(function installCamoDevtoolsConsoleCollector() {
|
|
76
|
+
const KEY = '__camo_console_collector_v1__';
|
|
77
|
+
const BUFFER_KEY = '__camo_console_buffer_v1__';
|
|
78
|
+
const MAX = ${Math.max(100, Math.floor(maxEntries || MAX_LIMIT))};
|
|
79
|
+
const now = () => Date.now();
|
|
80
|
+
|
|
81
|
+
const stringify = (value) => {
|
|
82
|
+
if (typeof value === 'string') return value;
|
|
83
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
|
84
|
+
if (value === null) return 'null';
|
|
85
|
+
if (typeof value === 'undefined') return 'undefined';
|
|
86
|
+
if (typeof value === 'function') return '[function]';
|
|
87
|
+
if (typeof value === 'symbol') return String(value);
|
|
88
|
+
if (value instanceof Error) return value.stack || value.message || String(value);
|
|
89
|
+
try {
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
} catch {
|
|
92
|
+
return Object.prototype.toString.call(value);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const pushEntry = (level, args) => {
|
|
97
|
+
const target = window[BUFFER_KEY];
|
|
98
|
+
if (!Array.isArray(target)) return;
|
|
99
|
+
const text = Array.from(args || []).map(stringify).join(' ');
|
|
100
|
+
target.push({
|
|
101
|
+
ts: now(),
|
|
102
|
+
level,
|
|
103
|
+
text,
|
|
104
|
+
href: String(window.location?.href || ''),
|
|
105
|
+
});
|
|
106
|
+
if (target.length > MAX) {
|
|
107
|
+
target.splice(0, target.length - MAX);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(window[BUFFER_KEY])) {
|
|
112
|
+
window[BUFFER_KEY] = [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!window[KEY]) {
|
|
116
|
+
const levels = ['log', 'info', 'warn', 'error', 'debug'];
|
|
117
|
+
const originals = {};
|
|
118
|
+
for (const level of levels) {
|
|
119
|
+
const raw = typeof console[level] === 'function' ? console[level] : console.log;
|
|
120
|
+
originals[level] = raw.bind(console);
|
|
121
|
+
console[level] = (...args) => {
|
|
122
|
+
try {
|
|
123
|
+
pushEntry(level, args);
|
|
124
|
+
} catch {}
|
|
125
|
+
return originals[level](...args);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
window.addEventListener('error', (event) => {
|
|
130
|
+
try {
|
|
131
|
+
const message = event?.message || 'window.error';
|
|
132
|
+
pushEntry('error', [message]);
|
|
133
|
+
} catch {}
|
|
134
|
+
});
|
|
135
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
136
|
+
try {
|
|
137
|
+
const reason = event?.reason instanceof Error
|
|
138
|
+
? (event.reason.stack || event.reason.message)
|
|
139
|
+
: stringify(event?.reason);
|
|
140
|
+
pushEntry('error', ['unhandledrejection', reason]);
|
|
141
|
+
} catch {}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
window[KEY] = { installedAt: now(), max: MAX };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
installed: true,
|
|
150
|
+
entries: Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY].length : 0,
|
|
151
|
+
max: MAX,
|
|
152
|
+
};
|
|
153
|
+
})();`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildConsoleReadScript(options = {}) {
|
|
157
|
+
const limit = clamp(parseNumber(options.limit, DEFAULT_LIMIT), 1, MAX_LIMIT);
|
|
158
|
+
const sinceTs = Math.max(0, parseNumber(options.sinceTs, 0) || 0);
|
|
159
|
+
const clear = options.clear === true;
|
|
160
|
+
const levels = Array.isArray(options.levels)
|
|
161
|
+
? options.levels.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
|
162
|
+
: [];
|
|
163
|
+
const levelsLiteral = JSON.stringify(levels);
|
|
164
|
+
|
|
165
|
+
return `(function readCamoDevtoolsConsole() {
|
|
166
|
+
const BUFFER_KEY = '__camo_console_buffer_v1__';
|
|
167
|
+
const raw = Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY] : [];
|
|
168
|
+
const levelSet = new Set(${levelsLiteral});
|
|
169
|
+
const list = raw.filter((entry) => {
|
|
170
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
171
|
+
const ts = Number(entry.ts || 0);
|
|
172
|
+
if (ts < ${sinceTs}) return false;
|
|
173
|
+
if (levelSet.size === 0) return true;
|
|
174
|
+
return levelSet.has(String(entry.level || '').toLowerCase());
|
|
175
|
+
});
|
|
176
|
+
const entries = list.slice(Math.max(0, list.length - ${limit}));
|
|
177
|
+
if (${clear ? 'true' : 'false'}) {
|
|
178
|
+
window[BUFFER_KEY] = [];
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
total: raw.length,
|
|
183
|
+
returned: entries.length,
|
|
184
|
+
sinceTs: ${sinceTs},
|
|
185
|
+
levels: Array.from(levelSet),
|
|
186
|
+
cleared: ${clear ? 'true' : 'false'},
|
|
187
|
+
entries,
|
|
188
|
+
};
|
|
189
|
+
})();`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildEvalScript(expression) {
|
|
193
|
+
return `(async function runCamoDevtoolsEval() {
|
|
194
|
+
const expr = ${JSON.stringify(expression || '')};
|
|
195
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
196
|
+
const resultPayload = { ok: true, mode: 'expression', value: null, valueType: null };
|
|
197
|
+
|
|
198
|
+
const toSerializable = (value, depth = 0, seen = new WeakSet()) => {
|
|
199
|
+
if (value === null) return null;
|
|
200
|
+
if (typeof value === 'undefined') return '[undefined]';
|
|
201
|
+
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
202
|
+
if (typeof value === 'bigint') return value.toString();
|
|
203
|
+
if (typeof value === 'function') return '[function]';
|
|
204
|
+
if (typeof value === 'symbol') return value.toString();
|
|
205
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack || null };
|
|
206
|
+
if (depth >= 3) return '[max-depth]';
|
|
207
|
+
if (Array.isArray(value)) return value.slice(0, 30).map((item) => toSerializable(item, depth + 1, seen));
|
|
208
|
+
if (typeof value === 'object') {
|
|
209
|
+
if (seen.has(value)) return '[circular]';
|
|
210
|
+
seen.add(value);
|
|
211
|
+
const out = {};
|
|
212
|
+
const keys = Object.keys(value).slice(0, 30);
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
out[key] = toSerializable(value[key], depth + 1, seen);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
return JSON.parse(JSON.stringify(value));
|
|
220
|
+
} catch {
|
|
221
|
+
return String(value);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const fn = new AsyncFunction('return (' + expr + ')');
|
|
227
|
+
const value = await fn();
|
|
228
|
+
resultPayload.value = toSerializable(value);
|
|
229
|
+
resultPayload.valueType = typeof value;
|
|
230
|
+
return resultPayload;
|
|
231
|
+
} catch (exprError) {
|
|
232
|
+
try {
|
|
233
|
+
const fn = new AsyncFunction(expr);
|
|
234
|
+
const value = await fn();
|
|
235
|
+
resultPayload.mode = 'statement';
|
|
236
|
+
resultPayload.value = toSerializable(value);
|
|
237
|
+
resultPayload.valueType = typeof value;
|
|
238
|
+
return resultPayload;
|
|
239
|
+
} catch (statementError) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
mode: 'statement',
|
|
243
|
+
error: {
|
|
244
|
+
message: statementError?.message || String(statementError),
|
|
245
|
+
stack: statementError?.stack || null,
|
|
246
|
+
expressionError: exprError?.message || String(exprError),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})();`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function ensureConsoleCollector(profileId, maxEntries = MAX_LIMIT) {
|
|
255
|
+
return callAPI('evaluate', {
|
|
256
|
+
profileId,
|
|
257
|
+
script: buildConsoleInstallScript(maxEntries),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handleLogs(args) {
|
|
262
|
+
const { profileId } = pickProfileAndExpression(args, 'logs');
|
|
263
|
+
if (!profileId) {
|
|
264
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
265
|
+
}
|
|
266
|
+
const limit = clamp(parseNumber(readFlagValue(args, ['--limit', '-n']), DEFAULT_LIMIT), 1, MAX_LIMIT);
|
|
267
|
+
const sinceTs = Math.max(0, parseNumber(readFlagValue(args, ['--since']), 0) || 0);
|
|
268
|
+
const levelsRaw = readFlagValue(args, ['--levels', '--level']) || '';
|
|
269
|
+
const levels = levelsRaw
|
|
270
|
+
.split(',')
|
|
271
|
+
.map((item) => String(item || '').trim().toLowerCase())
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
const clear = args.includes('--clear');
|
|
274
|
+
|
|
275
|
+
const install = await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
276
|
+
const result = await callAPI('evaluate', {
|
|
277
|
+
profileId,
|
|
278
|
+
script: buildConsoleReadScript({ limit, sinceTs, levels, clear }),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
console.log(JSON.stringify({
|
|
282
|
+
ok: true,
|
|
283
|
+
command: 'devtools.logs',
|
|
284
|
+
profileId,
|
|
285
|
+
collector: install?.result || install?.data || install || null,
|
|
286
|
+
result: result?.result || result?.data || result || null,
|
|
287
|
+
}, null, 2));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function handleClear(args) {
|
|
291
|
+
const { profileId } = pickProfileAndExpression(args, 'clear');
|
|
292
|
+
if (!profileId) {
|
|
293
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
294
|
+
}
|
|
295
|
+
await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
296
|
+
const result = await callAPI('evaluate', {
|
|
297
|
+
profileId,
|
|
298
|
+
script: buildConsoleReadScript({ limit: MAX_LIMIT, sinceTs: 0, clear: true }),
|
|
299
|
+
});
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
ok: true,
|
|
302
|
+
command: 'devtools.clear',
|
|
303
|
+
profileId,
|
|
304
|
+
result: result?.result || result?.data || result || null,
|
|
305
|
+
}, null, 2));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function handleEval(args) {
|
|
309
|
+
const { profileId, expression } = pickProfileAndExpression(args, 'eval');
|
|
310
|
+
if (!profileId) {
|
|
311
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
312
|
+
}
|
|
313
|
+
if (!expression) {
|
|
314
|
+
throw new Error('Usage: camo devtools eval [profileId] <expression> [--profile <id>]');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await ensureConsoleCollector(profileId, MAX_LIMIT);
|
|
318
|
+
const result = await callAPI('evaluate', {
|
|
319
|
+
profileId,
|
|
320
|
+
script: buildEvalScript(expression),
|
|
321
|
+
});
|
|
322
|
+
console.log(JSON.stringify({
|
|
323
|
+
ok: true,
|
|
324
|
+
command: 'devtools.eval',
|
|
325
|
+
profileId,
|
|
326
|
+
expression,
|
|
327
|
+
result: result?.result || result?.data || result || null,
|
|
328
|
+
}, null, 2));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function handleDevtoolsCommand(args) {
|
|
332
|
+
const sub = String(args[1] || '').trim().toLowerCase();
|
|
333
|
+
switch (sub) {
|
|
334
|
+
case 'logs':
|
|
335
|
+
return handleLogs(args);
|
|
336
|
+
case 'clear':
|
|
337
|
+
return handleClear(args);
|
|
338
|
+
case 'eval':
|
|
339
|
+
return handleEval(args);
|
|
340
|
+
default:
|
|
341
|
+
console.log(`Usage: camo devtools <logs|eval|clear> [options]
|
|
342
|
+
|
|
343
|
+
Commands:
|
|
344
|
+
logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
345
|
+
eval [profileId] <expression> [--profile <id>]
|
|
346
|
+
clear [profileId]
|
|
347
|
+
`);
|
|
348
|
+
}
|
|
349
|
+
}
|
package/src/utils/help.mjs
CHANGED
|
@@ -63,6 +63,11 @@ PAGES:
|
|
|
63
63
|
switch-page [profileId] <index>
|
|
64
64
|
list-pages [profileId]
|
|
65
65
|
|
|
66
|
+
DEVTOOLS:
|
|
67
|
+
devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
|
|
68
|
+
devtools eval [profileId] <expression> [--profile <id>]
|
|
69
|
+
devtools clear [profileId]
|
|
70
|
+
|
|
66
71
|
COOKIES:
|
|
67
72
|
cookies get [profileId] Get all cookies for profile
|
|
68
73
|
cookies save [profileId] --path <file> Save cookies to file
|
|
@@ -99,6 +104,8 @@ EXAMPLES:
|
|
|
99
104
|
camo start worker-1 --headless --alias shard1 --idle-timeout 45m
|
|
100
105
|
camo start worker-1 --devtools
|
|
101
106
|
camo start myprofile --width 1920 --height 1020
|
|
107
|
+
camo devtools eval myprofile "document.title"
|
|
108
|
+
camo devtools logs myprofile --levels error,warn --limit 50
|
|
102
109
|
camo stop --id inst_xxxxxxxx
|
|
103
110
|
camo stop --alias shard1
|
|
104
111
|
camo stop idle
|