@phnx-labs/agents-cli 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +793 -83
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +70 -1
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +224 -17
- package/dist/commands/prune.js +29 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +154 -20
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +78 -20
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +48 -36
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +32 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +41 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +22 -6
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +158 -23
- package/dist/lib/browser/profiles.d.ts +10 -2
- package/dist/lib/browser/profiles.js +122 -37
- package/dist/lib/browser/service.d.ts +91 -13
- package/dist/lib/browser/service.js +767 -132
- package/dist/lib/browser/types.d.ts +91 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +69 -14
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -15
- package/dist/lib/commands.js +11 -7
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +138 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1237 -22
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -66
- package/dist/lib/permissions.js +18 -18
- package/dist/lib/plugins.d.ts +94 -24
- package/dist/lib/plugins.js +702 -123
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +109 -5
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +55 -29
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +4 -52
- package/dist/lib/shims.js +23 -15
- package/dist/lib/skills.js +6 -2
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +101 -16
- package/dist/lib/state.js +179 -31
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +75 -17
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +158 -47
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +60 -59
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
package/dist/commands/browser.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { listProfiles, getProfile, createProfile, deleteProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, } from '../lib/browser/profiles.js';
|
|
4
|
+
import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
|
|
5
|
+
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
2
6
|
import { sendIPCRequest } from '../lib/browser/ipc.js';
|
|
3
|
-
import {
|
|
7
|
+
import { browserTaskPicker } from './browser-picker.js';
|
|
8
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
4
9
|
export function registerBrowserCommand(program) {
|
|
5
10
|
const browser = program
|
|
6
11
|
.command('browser')
|
|
@@ -27,11 +32,23 @@ function registerProfilesCommands(browser) {
|
|
|
27
32
|
console.log('Create one with: agents browser profiles create <name> --endpoint <url>');
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const hasDescriptions = allProfiles.some(p => p.description);
|
|
36
|
+
if (hasDescriptions) {
|
|
37
|
+
console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'DESCRIPTION'.padEnd(38) + 'ENDPOINTS');
|
|
38
|
+
console.log('-'.repeat(92));
|
|
39
|
+
for (const p of allProfiles) {
|
|
40
|
+
const endpoints = p.endpoints.join(', ');
|
|
41
|
+
const desc = (p.description ?? '').slice(0, 36).padEnd(38);
|
|
42
|
+
console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + desc + endpoints);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'ENDPOINTS');
|
|
47
|
+
console.log('-'.repeat(72));
|
|
48
|
+
for (const p of allProfiles) {
|
|
49
|
+
const endpoints = p.endpoints.join(', ');
|
|
50
|
+
console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints);
|
|
51
|
+
}
|
|
35
52
|
}
|
|
36
53
|
});
|
|
37
54
|
const VALID_BROWSERS = ['chrome', 'comet', 'chromium', 'brave', 'edge'];
|
|
@@ -39,10 +56,12 @@ function registerProfilesCommands(browser) {
|
|
|
39
56
|
.command('create <name>')
|
|
40
57
|
.description('Create a new browser profile')
|
|
41
58
|
.requiredOption('-b, --browser <type>', `Browser type: ${VALID_BROWSERS.join(', ')}`)
|
|
42
|
-
.
|
|
59
|
+
.option('-e, --endpoint <url>', 'CDP endpoint URL (repeatable; auto-assigned if omitted)', collect, [])
|
|
43
60
|
.option('-s, --secrets <bundle>', 'Secrets bundle to inject')
|
|
44
61
|
.option('-d, --description <text>', 'Profile description')
|
|
45
62
|
.option('--headless', 'Run in headless mode')
|
|
63
|
+
.option('--window <WxH>', 'Window size, e.g. 1512x982')
|
|
64
|
+
.option('--position <X,Y>', 'Window position on screen, e.g. 80,80')
|
|
46
65
|
.action(async (name, opts) => {
|
|
47
66
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
48
67
|
console.error('Profile name must be lowercase alphanumeric with hyphens');
|
|
@@ -52,13 +71,43 @@ function registerProfilesCommands(browser) {
|
|
|
52
71
|
console.error(`Invalid browser type. Must be one of: ${VALID_BROWSERS.join(', ')}`);
|
|
53
72
|
process.exit(1);
|
|
54
73
|
}
|
|
74
|
+
// Auto-assign a free port if no endpoint was provided
|
|
75
|
+
let endpoints = opts.endpoint;
|
|
76
|
+
if (endpoints.length === 0) {
|
|
77
|
+
const freePort = await findFreeProfilePort();
|
|
78
|
+
endpoints = [`cdp://127.0.0.1:${freePort}`];
|
|
79
|
+
}
|
|
80
|
+
// Viewport is mandatory — default to 1512x982 if --window is not provided
|
|
81
|
+
let viewport = {
|
|
82
|
+
width: 1512,
|
|
83
|
+
height: 982,
|
|
84
|
+
};
|
|
85
|
+
if (opts.window) {
|
|
86
|
+
const m = String(opts.window).match(/^(\d+)x(\d+)$/);
|
|
87
|
+
if (!m) {
|
|
88
|
+
console.error('--window must be WxH, e.g. 1512x982');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
viewport.width = parseInt(m[1], 10);
|
|
92
|
+
viewport.height = parseInt(m[2], 10);
|
|
93
|
+
}
|
|
94
|
+
if (opts.position) {
|
|
95
|
+
const m = String(opts.position).match(/^(-?\d+),(-?\d+)$/);
|
|
96
|
+
if (!m) {
|
|
97
|
+
console.error('--position must be X,Y, e.g. 80,80');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
viewport.x = parseInt(m[1], 10);
|
|
101
|
+
viewport.y = parseInt(m[2], 10);
|
|
102
|
+
}
|
|
55
103
|
const profile = {
|
|
56
104
|
name,
|
|
57
105
|
description: opts.description,
|
|
58
106
|
browser: opts.browser,
|
|
59
|
-
endpoints
|
|
107
|
+
endpoints,
|
|
60
108
|
secrets: opts.secrets,
|
|
61
109
|
chrome: opts.headless ? { headless: true } : undefined,
|
|
110
|
+
viewport,
|
|
62
111
|
};
|
|
63
112
|
await createProfile(profile);
|
|
64
113
|
console.log(`Created profile: ${name}`);
|
|
@@ -66,12 +115,22 @@ function registerProfilesCommands(browser) {
|
|
|
66
115
|
profiles
|
|
67
116
|
.command('show <name>')
|
|
68
117
|
.description('Show profile details')
|
|
69
|
-
.
|
|
118
|
+
.option('--json', 'Output machine-readable JSON')
|
|
119
|
+
.action(async (name, opts) => {
|
|
70
120
|
const profile = await getProfile(name);
|
|
71
121
|
if (!profile) {
|
|
72
|
-
|
|
122
|
+
if (opts.json) {
|
|
123
|
+
console.log(JSON.stringify({ ok: false, error: `Profile "${name}" not found` }));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.error(`Profile "${name}" not found`);
|
|
127
|
+
}
|
|
73
128
|
process.exit(1);
|
|
74
129
|
}
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
75
134
|
console.log(`Name: ${profile.name}`);
|
|
76
135
|
console.log(`Browser: ${profile.browser}`);
|
|
77
136
|
if (profile.description)
|
|
@@ -92,27 +151,211 @@ function registerProfilesCommands(browser) {
|
|
|
92
151
|
await deleteProfile(name);
|
|
93
152
|
console.log(`Deleted profile: ${name}`);
|
|
94
153
|
});
|
|
154
|
+
profiles
|
|
155
|
+
.command('launch <name>')
|
|
156
|
+
.description('Start (or attach to) the profile\'s browser without creating a task')
|
|
157
|
+
.action(async (name) => {
|
|
158
|
+
const profile = await getProfile(name);
|
|
159
|
+
if (!profile) {
|
|
160
|
+
console.error(`Profile "${name}" not found`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const response = await sendIPCRequest({
|
|
164
|
+
action: 'launch-profile',
|
|
165
|
+
profile: name,
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
console.error(response.error);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
|
|
172
|
+
console.log(`Launched "${name}" on port ${response.port} (${pidLabel})`);
|
|
173
|
+
console.log(`Next: agents browser start --profile ${name} --url <url>`);
|
|
174
|
+
});
|
|
175
|
+
profiles
|
|
176
|
+
.command('doctor <name>')
|
|
177
|
+
.description('Diagnose a browser profile: binary, port, user-data-dir, onboarding state')
|
|
178
|
+
.action(async (name) => {
|
|
179
|
+
const profile = await getProfile(name);
|
|
180
|
+
if (!profile) {
|
|
181
|
+
console.error(`Profile "${name}" not found`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const checks = [];
|
|
185
|
+
// 1. Binary exists for declared browser type
|
|
186
|
+
try {
|
|
187
|
+
const binPath = findBrowserPath(profile.browser, profile.binary);
|
|
188
|
+
checks.push({ label: 'binary', ok: true, detail: binPath });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
checks.push({
|
|
192
|
+
label: 'binary',
|
|
193
|
+
ok: false,
|
|
194
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// 2. Configured port: free, or already serving the expected browser?
|
|
198
|
+
const port = extractConfiguredPort(profile);
|
|
199
|
+
let attachingToExistingBrowser = false;
|
|
200
|
+
if (port === undefined) {
|
|
201
|
+
checks.push({ label: 'port', ok: true, detail: 'no port in endpoint' });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const occupant = getPortOccupant(port);
|
|
205
|
+
if (!occupant) {
|
|
206
|
+
checks.push({ label: 'port', ok: true, detail: `${port} is free` });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
try {
|
|
210
|
+
const { browser } = await discoverBrowserWsUrl(port);
|
|
211
|
+
verifyBrowserIdentity(browser, profile.browser, port);
|
|
212
|
+
checks.push({
|
|
213
|
+
label: 'port',
|
|
214
|
+
ok: true,
|
|
215
|
+
detail: `${port} serving ${browser} (pid ${occupant.pid})`,
|
|
216
|
+
});
|
|
217
|
+
attachingToExistingBrowser = true;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
221
|
+
checks.push({
|
|
222
|
+
label: 'port',
|
|
223
|
+
ok: false,
|
|
224
|
+
detail: `${port} taken by ${occupant.command} (pid ${occupant.pid}) — ${msg}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// 3. User-data-dir exists and is writable
|
|
230
|
+
const userDataDir = path.join(getProfileRuntimeDir(name), 'chrome-data');
|
|
231
|
+
try {
|
|
232
|
+
if (!fs.existsSync(userDataDir)) {
|
|
233
|
+
checks.push({
|
|
234
|
+
label: 'user-data-dir',
|
|
235
|
+
ok: true,
|
|
236
|
+
detail: `will be created at ${userDataDir}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
fs.accessSync(userDataDir, fs.constants.W_OK);
|
|
241
|
+
checks.push({ label: 'user-data-dir', ok: true, detail: userDataDir });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
checks.push({
|
|
246
|
+
label: 'user-data-dir',
|
|
247
|
+
ok: false,
|
|
248
|
+
detail: `${userDataDir} not writable: ${err instanceof Error ? err.message : err}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// 4. Onboarding heuristic — only meaningful when WE will launch the
|
|
252
|
+
// browser. When the configured port is already serving a debuggable
|
|
253
|
+
// browser, that browser owns its own user-data-dir and the priming
|
|
254
|
+
// status of our managed dir is irrelevant.
|
|
255
|
+
if (attachingToExistingBrowser) {
|
|
256
|
+
checks.push({
|
|
257
|
+
label: 'onboarding',
|
|
258
|
+
ok: true,
|
|
259
|
+
detail: 'n/a (attaching to existing browser)',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
264
|
+
if (fs.existsSync(localStatePath)) {
|
|
265
|
+
const size = fs.statSync(localStatePath).size;
|
|
266
|
+
if (size > 0) {
|
|
267
|
+
checks.push({ label: 'onboarding', ok: true, detail: 'Local State present' });
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
checks.push({
|
|
271
|
+
label: 'onboarding',
|
|
272
|
+
ok: false,
|
|
273
|
+
detail: 'Local State is empty — run `agents browser profiles prime ' + name + '`',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
checks.push({
|
|
279
|
+
label: 'onboarding',
|
|
280
|
+
ok: false,
|
|
281
|
+
detail: 'Not primed yet — run `agents browser profiles prime ' + name + '`',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const allOk = checks.every((c) => c.ok);
|
|
286
|
+
for (const c of checks) {
|
|
287
|
+
const marker = c.ok ? 'OK ' : 'FAIL';
|
|
288
|
+
console.log(`${marker} ${c.label.padEnd(15)} ${c.detail}`);
|
|
289
|
+
}
|
|
290
|
+
if (!allOk)
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
|
293
|
+
profiles
|
|
294
|
+
.command('prime <name>')
|
|
295
|
+
.description('Launch the profile so you can complete first-run onboarding interactively')
|
|
296
|
+
.action(async (name) => {
|
|
297
|
+
const profile = await getProfile(name);
|
|
298
|
+
if (!profile) {
|
|
299
|
+
console.error(`Profile "${name}" not found`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
const response = await sendIPCRequest({
|
|
303
|
+
action: 'launch-profile',
|
|
304
|
+
profile: name,
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
console.error(response.error);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
|
|
311
|
+
console.log(`Launched "${name}" on port ${response.port} (${pidLabel}).`);
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log('Finish any first-run / onboarding screens in the browser window');
|
|
314
|
+
console.log('(welcome, profile setup, default-browser prompt, sign-in, etc.).');
|
|
315
|
+
console.log('Once you reach a normal browsing surface, this profile is primed');
|
|
316
|
+
console.log('— its user-data-dir persists across runs, so you only do this once.');
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log(`Next: agents browser start --profile ${name} --url <url>`);
|
|
319
|
+
});
|
|
95
320
|
}
|
|
96
321
|
function registerTaskCommands(browser) {
|
|
97
322
|
browser
|
|
98
|
-
.command('start
|
|
99
|
-
.description('Start a browser task')
|
|
323
|
+
.command('start')
|
|
324
|
+
.description('Start a browser task with a profile')
|
|
100
325
|
.requiredOption('-p, --profile <name>', 'Browser profile to use')
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
326
|
+
.option('-t, --task <name>', 'Task name (auto-generated if omitted)')
|
|
327
|
+
.option('-u, --url <url>', 'Open URL in first tab')
|
|
328
|
+
.action(async (opts) => {
|
|
106
329
|
const response = await sendIPCRequest({
|
|
107
330
|
action: 'start',
|
|
108
331
|
profile: opts.profile,
|
|
332
|
+
taskName: opts.task,
|
|
333
|
+
url: opts.url,
|
|
334
|
+
});
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
console.error(response.error);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
if (opts.url && response.tabId) {
|
|
340
|
+
console.log(`Started task "${response.task}" with tab ${response.tabId}`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
console.log(`Started task "${response.task}"`);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
browser
|
|
347
|
+
.command('done <task>')
|
|
348
|
+
.description('Complete a task and close its tabs')
|
|
349
|
+
.action(async (task) => {
|
|
350
|
+
const response = await sendIPCRequest({
|
|
351
|
+
action: 'done',
|
|
109
352
|
task,
|
|
110
353
|
});
|
|
111
354
|
if (!response.ok) {
|
|
112
355
|
console.error(response.error);
|
|
113
356
|
process.exit(1);
|
|
114
357
|
}
|
|
115
|
-
console.log(
|
|
358
|
+
console.log(`Completed task: ${task}`);
|
|
116
359
|
});
|
|
117
360
|
browser
|
|
118
361
|
.command('stop <task>')
|
|
@@ -130,7 +373,7 @@ function registerTaskCommands(browser) {
|
|
|
130
373
|
});
|
|
131
374
|
browser
|
|
132
375
|
.command('navigate <task> <url>')
|
|
133
|
-
.description('
|
|
376
|
+
.description('Navigate current tab to URL (creates tab if none exist)')
|
|
134
377
|
.option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
|
|
135
378
|
.action(async (task, url, opts) => {
|
|
136
379
|
const response = await sendIPCRequest({
|
|
@@ -143,41 +386,48 @@ function registerTaskCommands(browser) {
|
|
|
143
386
|
console.error(response.error);
|
|
144
387
|
process.exit(1);
|
|
145
388
|
}
|
|
146
|
-
console.log(`
|
|
389
|
+
console.log(`Navigated ${response.tabId} to ${url}`);
|
|
147
390
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
391
|
+
// Tab subcommand group
|
|
392
|
+
const tab = browser.command('tab').description('Manage tabs');
|
|
393
|
+
tab
|
|
394
|
+
.command('add <task> <url>')
|
|
395
|
+
.description('Open URL in new tab (becomes current)')
|
|
396
|
+
.option('-p, --profile <name>', 'Browser profile')
|
|
397
|
+
.action(async (task, url, opts) => {
|
|
153
398
|
const response = await sendIPCRequest({
|
|
154
|
-
action: '
|
|
399
|
+
action: 'tab-add',
|
|
155
400
|
task,
|
|
401
|
+
url,
|
|
156
402
|
profile: opts.profile,
|
|
157
403
|
});
|
|
158
404
|
if (!response.ok) {
|
|
159
405
|
console.error(response.error);
|
|
160
406
|
process.exit(1);
|
|
161
407
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
408
|
+
console.log(`Opened tab ${response.tabId}: ${url}`);
|
|
409
|
+
});
|
|
410
|
+
tab
|
|
411
|
+
.command('focus <task> <tabId>')
|
|
412
|
+
.description('Switch to tab (by ID, prefix, or URL substring)')
|
|
413
|
+
.action(async (task, tabId) => {
|
|
414
|
+
const response = await sendIPCRequest({
|
|
415
|
+
action: 'tab-focus',
|
|
416
|
+
task,
|
|
417
|
+
tabId,
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
console.error(response.error);
|
|
421
|
+
process.exit(1);
|
|
173
422
|
}
|
|
423
|
+
console.log(`Focused tab ${response.tabId}`);
|
|
174
424
|
});
|
|
175
|
-
|
|
425
|
+
tab
|
|
176
426
|
.command('close <task> [tabId]')
|
|
177
|
-
.description('Close
|
|
427
|
+
.description('Close tab(s) — omit tabId to close all')
|
|
178
428
|
.action(async (task, tabId) => {
|
|
179
429
|
const response = await sendIPCRequest({
|
|
180
|
-
action: 'close',
|
|
430
|
+
action: 'tab-close',
|
|
181
431
|
task,
|
|
182
432
|
tabId,
|
|
183
433
|
});
|
|
@@ -185,7 +435,40 @@ function registerTaskCommands(browser) {
|
|
|
185
435
|
console.error(response.error);
|
|
186
436
|
process.exit(1);
|
|
187
437
|
}
|
|
188
|
-
console.log(tabId ? `Closed tab ${tabId}` : `Closed all tabs for
|
|
438
|
+
console.log(tabId ? `Closed tab ${tabId}` : `Closed all tabs for ${task}`);
|
|
439
|
+
});
|
|
440
|
+
tab
|
|
441
|
+
.command('list <task>')
|
|
442
|
+
.description('List tabs for a task')
|
|
443
|
+
.option('--json', 'Output machine-readable JSON')
|
|
444
|
+
.action(async (task, opts) => {
|
|
445
|
+
const response = await sendIPCRequest({
|
|
446
|
+
action: 'tab-list',
|
|
447
|
+
task,
|
|
448
|
+
});
|
|
449
|
+
if (!response.ok) {
|
|
450
|
+
if (opts.json) {
|
|
451
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
console.error(response.error);
|
|
455
|
+
}
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
if (opts.json) {
|
|
459
|
+
console.log(JSON.stringify(response.tabs ?? [], null, 2));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!response.tabs || response.tabs.length === 0) {
|
|
463
|
+
console.log('No tabs open');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
console.log('TAB'.padEnd(12) + 'URL');
|
|
467
|
+
console.log('-'.repeat(70));
|
|
468
|
+
for (const t of response.tabs) {
|
|
469
|
+
const current = t.current ? ' *' : '';
|
|
470
|
+
console.log(t.id.padEnd(12) + t.url.slice(0, 55) + current);
|
|
471
|
+
}
|
|
189
472
|
});
|
|
190
473
|
browser
|
|
191
474
|
.command('screenshot <task> [tabId]')
|
|
@@ -205,13 +488,14 @@ function registerTaskCommands(browser) {
|
|
|
205
488
|
console.log(response.path);
|
|
206
489
|
});
|
|
207
490
|
browser
|
|
208
|
-
.command('evaluate <task> <
|
|
209
|
-
.description('Evaluate JavaScript in
|
|
210
|
-
.
|
|
491
|
+
.command('evaluate <task> <expression>')
|
|
492
|
+
.description('Evaluate JavaScript in current tab')
|
|
493
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
494
|
+
.action(async (task, expression, opts) => {
|
|
211
495
|
const response = await sendIPCRequest({
|
|
212
496
|
action: 'evaluate',
|
|
213
497
|
task,
|
|
214
|
-
tabId,
|
|
498
|
+
tabId: opts.tab,
|
|
215
499
|
expr: expression,
|
|
216
500
|
});
|
|
217
501
|
if (!response.ok) {
|
|
@@ -224,32 +508,101 @@ function registerTaskCommands(browser) {
|
|
|
224
508
|
.command('status')
|
|
225
509
|
.description('Show running browser tasks')
|
|
226
510
|
.option('-p, --profile <name>', 'Filter by profile')
|
|
511
|
+
.option('--json', 'Output machine-readable JSON')
|
|
227
512
|
.action(async (opts) => {
|
|
228
513
|
const response = await sendIPCRequest({
|
|
229
514
|
action: 'status',
|
|
230
515
|
profile: opts.profile,
|
|
231
516
|
});
|
|
232
517
|
if (!response.ok) {
|
|
233
|
-
|
|
518
|
+
if (opts.json) {
|
|
519
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
console.error(response.error);
|
|
523
|
+
}
|
|
234
524
|
process.exit(1);
|
|
235
525
|
}
|
|
236
|
-
if (
|
|
237
|
-
console.log(
|
|
526
|
+
if (opts.json) {
|
|
527
|
+
console.log(JSON.stringify(response.profiles ?? [], null, 2));
|
|
238
528
|
return;
|
|
239
529
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
530
|
+
// Build flat list of tasks with profile context
|
|
531
|
+
const allTasks = [];
|
|
532
|
+
for (const profile of response.profiles || []) {
|
|
533
|
+
for (const task of profile.tasks) {
|
|
534
|
+
allTasks.push({ task, profile });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (allTasks.length === 0) {
|
|
538
|
+
// Show recent history instead
|
|
539
|
+
const historyResponse = await sendIPCRequest({ action: 'history', limit: 5 });
|
|
540
|
+
if (historyResponse.ok && historyResponse.history && historyResponse.history.length > 0) {
|
|
541
|
+
console.log('No active tasks. Recent history:\n');
|
|
542
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
543
|
+
console.log('-'.repeat(75));
|
|
544
|
+
for (const h of historyResponse.history) {
|
|
545
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
546
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
547
|
+
const ended = formatAge(h.endedAt);
|
|
548
|
+
console.log(h.profile.padEnd(15) +
|
|
549
|
+
h.name.padEnd(18) +
|
|
550
|
+
domains.slice(0, 20).padEnd(22) +
|
|
551
|
+
duration.padEnd(10) +
|
|
552
|
+
ended);
|
|
553
|
+
}
|
|
554
|
+
console.log('\nRun `browser history` for more.');
|
|
244
555
|
}
|
|
245
556
|
else {
|
|
246
|
-
console.log('
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
557
|
+
console.log('No browser tasks running');
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Interactive picker for TTY, plain output otherwise
|
|
562
|
+
if (isInteractiveTerminal()) {
|
|
563
|
+
const picked = await browserTaskPicker({
|
|
564
|
+
message: 'Browser tasks:',
|
|
565
|
+
tasks: allTasks,
|
|
566
|
+
});
|
|
567
|
+
if (picked) {
|
|
568
|
+
// Show tab list for the selected task
|
|
569
|
+
const tabResponse = await sendIPCRequest({
|
|
570
|
+
action: 'tab-list',
|
|
571
|
+
task: picked.task.task.name,
|
|
572
|
+
});
|
|
573
|
+
if (tabResponse.ok && tabResponse.tabs) {
|
|
574
|
+
console.log(`\nTabs for ${picked.task.task.name}:`);
|
|
575
|
+
for (const tab of tabResponse.tabs) {
|
|
576
|
+
console.log(` ${tab.id} ${tab.url}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// Non-interactive: simple table output
|
|
583
|
+
for (const profile of response.profiles || []) {
|
|
584
|
+
const portLabel = profile.configuredPort && profile.configuredPort !== profile.port
|
|
585
|
+
? `port ${profile.port} (configured ${profile.configuredPort})`
|
|
586
|
+
: `port ${profile.port}`;
|
|
587
|
+
// pid 0 means the daemon attached to a browser we didn't launch — no
|
|
588
|
+
// tracked pid. Render it as "attached" rather than the literal 0.
|
|
589
|
+
const pidLabel = profile.pid ? `pid ${profile.pid}` : 'attached';
|
|
590
|
+
console.log(`\n${profile.name} (${portLabel}, ${pidLabel})`);
|
|
591
|
+
if (profile.tasks.length === 0) {
|
|
592
|
+
console.log(' No active tasks');
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
console.log(' TASK'.padEnd(20) + 'TABS'.padEnd(6) + 'DOMAINS'.padEnd(25) + 'CREATED');
|
|
596
|
+
for (const task of profile.tasks) {
|
|
597
|
+
const age = formatAge(task.createdAt);
|
|
598
|
+
const name = task.name || task.id;
|
|
599
|
+
const domains = task.domains?.slice(0, 2).join(', ') || '-';
|
|
600
|
+
console.log(' ' +
|
|
601
|
+
name.padEnd(18) +
|
|
602
|
+
String(task.tabCount).padEnd(6) +
|
|
603
|
+
domains.slice(0, 23).padEnd(25) +
|
|
604
|
+
age);
|
|
605
|
+
}
|
|
253
606
|
}
|
|
254
607
|
}
|
|
255
608
|
}
|
|
@@ -258,13 +611,19 @@ function registerTaskCommands(browser) {
|
|
|
258
611
|
.command('tasks')
|
|
259
612
|
.description('List all browser tasks')
|
|
260
613
|
.option('-p, --profile <name>', 'Filter by profile')
|
|
614
|
+
.option('--json', 'Output machine-readable JSON')
|
|
261
615
|
.action(async (opts) => {
|
|
262
616
|
const response = await sendIPCRequest({
|
|
263
617
|
action: 'status',
|
|
264
618
|
profile: opts.profile,
|
|
265
619
|
});
|
|
266
620
|
if (!response.ok) {
|
|
267
|
-
|
|
621
|
+
if (opts.json) {
|
|
622
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.error(response.error);
|
|
626
|
+
}
|
|
268
627
|
process.exit(1);
|
|
269
628
|
}
|
|
270
629
|
const allTasks = [];
|
|
@@ -272,35 +631,102 @@ function registerTaskCommands(browser) {
|
|
|
272
631
|
for (const task of profile.tasks) {
|
|
273
632
|
allTasks.push({
|
|
274
633
|
profile: profile.name,
|
|
275
|
-
|
|
634
|
+
name: task.name || task.id,
|
|
276
635
|
tabs: task.tabCount,
|
|
636
|
+
domains: task.domains || [],
|
|
277
637
|
created: task.createdAt,
|
|
278
638
|
});
|
|
279
639
|
}
|
|
280
640
|
}
|
|
641
|
+
if (opts.json) {
|
|
642
|
+
console.log(JSON.stringify(allTasks, null, 2));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
281
645
|
if (allTasks.length === 0) {
|
|
282
|
-
|
|
646
|
+
// Show recent history instead
|
|
647
|
+
const historyResponse = await sendIPCRequest({ action: 'history', limit: 5 });
|
|
648
|
+
if (historyResponse.ok && historyResponse.history && historyResponse.history.length > 0) {
|
|
649
|
+
console.log('No active tasks. Recent history:\n');
|
|
650
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
651
|
+
console.log('-'.repeat(75));
|
|
652
|
+
for (const h of historyResponse.history) {
|
|
653
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
654
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
655
|
+
const ended = formatAge(h.endedAt);
|
|
656
|
+
console.log(h.profile.padEnd(15) +
|
|
657
|
+
h.name.padEnd(18) +
|
|
658
|
+
domains.slice(0, 20).padEnd(22) +
|
|
659
|
+
duration.padEnd(10) +
|
|
660
|
+
ended);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
console.log('No active tasks');
|
|
665
|
+
}
|
|
283
666
|
return;
|
|
284
667
|
}
|
|
285
|
-
console.log('PROFILE'.padEnd(
|
|
286
|
-
console.log('-'.repeat(
|
|
668
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'TABS'.padEnd(6) + 'DOMAINS'.padEnd(22) + 'CREATED');
|
|
669
|
+
console.log('-'.repeat(70));
|
|
287
670
|
for (const t of allTasks) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
671
|
+
const domains = t.domains.slice(0, 2).join(', ') || '-';
|
|
672
|
+
console.log(t.profile.padEnd(15) +
|
|
673
|
+
t.name.padEnd(18) +
|
|
674
|
+
String(t.tabs).padEnd(6) +
|
|
675
|
+
domains.slice(0, 20).padEnd(22) +
|
|
291
676
|
formatAge(t.created));
|
|
292
677
|
}
|
|
293
678
|
});
|
|
294
679
|
browser
|
|
295
|
-
.command('
|
|
680
|
+
.command('history')
|
|
681
|
+
.description('Show recent browser task history')
|
|
682
|
+
.option('-l, --limit <n>', 'Number of entries (default 10)', '10')
|
|
683
|
+
.option('--json', 'Output machine-readable JSON')
|
|
684
|
+
.action(async (opts) => {
|
|
685
|
+
const response = await sendIPCRequest({
|
|
686
|
+
action: 'history',
|
|
687
|
+
limit: parseInt(opts.limit, 10),
|
|
688
|
+
});
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
if (opts.json) {
|
|
691
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
console.error(response.error);
|
|
695
|
+
}
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
if (opts.json) {
|
|
699
|
+
console.log(JSON.stringify(response.history ?? [], null, 2));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!response.history || response.history.length === 0) {
|
|
703
|
+
console.log('No browser task history');
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
707
|
+
console.log('-'.repeat(75));
|
|
708
|
+
for (const h of response.history) {
|
|
709
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
710
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
711
|
+
const ended = formatAge(h.endedAt);
|
|
712
|
+
console.log(h.profile.padEnd(15) +
|
|
713
|
+
h.name.padEnd(18) +
|
|
714
|
+
domains.slice(0, 20).padEnd(22) +
|
|
715
|
+
duration.padEnd(10) +
|
|
716
|
+
ended);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
browser
|
|
720
|
+
.command('refs <task>')
|
|
296
721
|
.description('Get DOM refs for interactive elements')
|
|
722
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
297
723
|
.option('--all', 'Include non-interactive elements')
|
|
298
724
|
.option('-l, --limit <n>', 'Max elements (default 500)', '500')
|
|
299
|
-
.action(async (task,
|
|
725
|
+
.action(async (task, opts) => {
|
|
300
726
|
const response = await sendIPCRequest({
|
|
301
727
|
action: 'refs',
|
|
302
728
|
task,
|
|
303
|
-
tabId,
|
|
729
|
+
tabId: opts.tab,
|
|
304
730
|
interactive: !opts.all,
|
|
305
731
|
limit: parseInt(opts.limit, 10),
|
|
306
732
|
});
|
|
@@ -311,13 +737,14 @@ function registerTaskCommands(browser) {
|
|
|
311
737
|
console.log(response.refs);
|
|
312
738
|
});
|
|
313
739
|
browser
|
|
314
|
-
.command('click <task> <
|
|
740
|
+
.command('click <task> <ref>')
|
|
315
741
|
.description('Click an element by ref')
|
|
316
|
-
.
|
|
742
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
743
|
+
.action(async (task, ref, opts) => {
|
|
317
744
|
const response = await sendIPCRequest({
|
|
318
745
|
action: 'click',
|
|
319
746
|
task,
|
|
320
|
-
tabId,
|
|
747
|
+
tabId: opts.tab,
|
|
321
748
|
ref: parseInt(ref, 10),
|
|
322
749
|
});
|
|
323
750
|
if (!response.ok) {
|
|
@@ -327,13 +754,14 @@ function registerTaskCommands(browser) {
|
|
|
327
754
|
console.log('Clicked');
|
|
328
755
|
});
|
|
329
756
|
browser
|
|
330
|
-
.command('type <task> <
|
|
757
|
+
.command('type <task> <ref> <text>')
|
|
331
758
|
.description('Type text into an element by ref')
|
|
332
|
-
.
|
|
759
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
760
|
+
.action(async (task, ref, text, opts) => {
|
|
333
761
|
const response = await sendIPCRequest({
|
|
334
762
|
action: 'type',
|
|
335
763
|
task,
|
|
336
|
-
tabId,
|
|
764
|
+
tabId: opts.tab,
|
|
337
765
|
ref: parseInt(ref, 10),
|
|
338
766
|
text,
|
|
339
767
|
});
|
|
@@ -344,13 +772,14 @@ function registerTaskCommands(browser) {
|
|
|
344
772
|
console.log('Typed');
|
|
345
773
|
});
|
|
346
774
|
browser
|
|
347
|
-
.command('press <task> <
|
|
775
|
+
.command('press <task> <key>')
|
|
348
776
|
.description('Press a key (Enter, Tab, Escape, etc)')
|
|
349
|
-
.
|
|
777
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
778
|
+
.action(async (task, key, opts) => {
|
|
350
779
|
const response = await sendIPCRequest({
|
|
351
780
|
action: 'press',
|
|
352
781
|
task,
|
|
353
|
-
tabId,
|
|
782
|
+
tabId: opts.tab,
|
|
354
783
|
key,
|
|
355
784
|
});
|
|
356
785
|
if (!response.ok) {
|
|
@@ -360,13 +789,14 @@ function registerTaskCommands(browser) {
|
|
|
360
789
|
console.log('Pressed');
|
|
361
790
|
});
|
|
362
791
|
browser
|
|
363
|
-
.command('hover <task> <
|
|
792
|
+
.command('hover <task> <ref>')
|
|
364
793
|
.description('Hover over an element by ref')
|
|
365
|
-
.
|
|
794
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
795
|
+
.action(async (task, ref, opts) => {
|
|
366
796
|
const response = await sendIPCRequest({
|
|
367
797
|
action: 'hover',
|
|
368
798
|
task,
|
|
369
|
-
tabId,
|
|
799
|
+
tabId: opts.tab,
|
|
370
800
|
ref: parseInt(ref, 10),
|
|
371
801
|
});
|
|
372
802
|
if (!response.ok) {
|
|
@@ -375,6 +805,275 @@ function registerTaskCommands(browser) {
|
|
|
375
805
|
}
|
|
376
806
|
console.log('Hovered');
|
|
377
807
|
});
|
|
808
|
+
browser
|
|
809
|
+
.command('scroll <task> <deltaX> <deltaY>')
|
|
810
|
+
.description('Scroll the page by pixel amount')
|
|
811
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
812
|
+
.option('-x, --at-x <x>', 'X coordinate to dispatch scroll from (default 0)', parseInt)
|
|
813
|
+
.option('-y, --at-y <y>', 'Y coordinate to dispatch scroll from (default 0)', parseInt)
|
|
814
|
+
.action(async (task, deltaX, deltaY, opts) => {
|
|
815
|
+
const response = await sendIPCRequest({
|
|
816
|
+
action: 'scroll',
|
|
817
|
+
task,
|
|
818
|
+
tabId: opts.tab,
|
|
819
|
+
scrollX: parseInt(deltaX, 10),
|
|
820
|
+
scrollY: parseInt(deltaY, 10),
|
|
821
|
+
scrollAtX: opts.atX,
|
|
822
|
+
scrollAtY: opts.atY,
|
|
823
|
+
});
|
|
824
|
+
if (!response.ok) {
|
|
825
|
+
console.error(response.error);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
console.log('Scrolled');
|
|
829
|
+
});
|
|
830
|
+
// ─── Viewport & Device ───────────────────────────────────────────────────────
|
|
831
|
+
const setCmd = browser.command('set').description('Set browser emulation options');
|
|
832
|
+
setCmd
|
|
833
|
+
.command('viewport <task> <width> <height>')
|
|
834
|
+
.description('Set viewport size')
|
|
835
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
836
|
+
.option('-m, --mobile', 'Enable mobile emulation')
|
|
837
|
+
.option('-s, --scale <factor>', 'Device scale factor', parseFloat)
|
|
838
|
+
.action(async (task, width, height, opts) => {
|
|
839
|
+
const response = await sendIPCRequest({
|
|
840
|
+
action: 'set-viewport',
|
|
841
|
+
task,
|
|
842
|
+
tabId: opts.tab,
|
|
843
|
+
width: parseInt(width, 10),
|
|
844
|
+
height: parseInt(height, 10),
|
|
845
|
+
mobile: opts.mobile,
|
|
846
|
+
deviceScaleFactor: opts.scale,
|
|
847
|
+
});
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
console.error(response.error);
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
console.log(`Viewport set to ${width}x${height}${opts.mobile ? ' (mobile)' : ''}`);
|
|
853
|
+
});
|
|
854
|
+
setCmd
|
|
855
|
+
.command('device <task> <device-name>')
|
|
856
|
+
.description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
|
|
857
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
858
|
+
.action(async (task, deviceName, opts) => {
|
|
859
|
+
const response = await sendIPCRequest({
|
|
860
|
+
action: 'set-device',
|
|
861
|
+
task,
|
|
862
|
+
tabId: opts.tab,
|
|
863
|
+
deviceName,
|
|
864
|
+
});
|
|
865
|
+
if (!response.ok) {
|
|
866
|
+
console.error(response.error);
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
console.log(`Device set to ${deviceName}`);
|
|
870
|
+
});
|
|
871
|
+
browser
|
|
872
|
+
.command('devices')
|
|
873
|
+
.description('List available device presets')
|
|
874
|
+
.action(async () => {
|
|
875
|
+
const { DEVICES } = await import('../lib/browser/devices.js');
|
|
876
|
+
console.log('Available devices:');
|
|
877
|
+
for (const [name, desc] of Object.entries(DEVICES)) {
|
|
878
|
+
console.log(` ${name.padEnd(16)} ${desc.width}x${desc.height} @${desc.deviceScaleFactor}x${desc.mobile ? ' (mobile)' : ''}`);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
// ─── Console & Errors ────────────────────────────────────────────────────────
|
|
882
|
+
browser
|
|
883
|
+
.command('console <task>')
|
|
884
|
+
.description('Read console logs from a tab')
|
|
885
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
886
|
+
.option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
|
|
887
|
+
.option('--clear', 'Clear logs after reading')
|
|
888
|
+
.action(async (task, opts) => {
|
|
889
|
+
const response = await sendIPCRequest({
|
|
890
|
+
action: 'console',
|
|
891
|
+
task,
|
|
892
|
+
tabId: opts.tab,
|
|
893
|
+
level: opts.level,
|
|
894
|
+
clear: opts.clear,
|
|
895
|
+
});
|
|
896
|
+
if (!response.ok) {
|
|
897
|
+
console.error(response.error);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
if (!response.logs || response.logs.length === 0) {
|
|
901
|
+
console.log('No console logs');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
for (const log of response.logs) {
|
|
905
|
+
const prefix = `[${log.level.toUpperCase()}]`.padEnd(8);
|
|
906
|
+
const loc = log.url ? ` (${log.url}${log.line ? `:${log.line}` : ''})` : '';
|
|
907
|
+
console.log(`${prefix} ${log.text}${loc}`);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
browser
|
|
911
|
+
.command('errors <task>')
|
|
912
|
+
.description('Read page errors from a tab')
|
|
913
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
914
|
+
.option('--clear', 'Clear errors after reading')
|
|
915
|
+
.action(async (task, opts) => {
|
|
916
|
+
const response = await sendIPCRequest({
|
|
917
|
+
action: 'errors',
|
|
918
|
+
task,
|
|
919
|
+
tabId: opts.tab,
|
|
920
|
+
clear: opts.clear,
|
|
921
|
+
});
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
console.error(response.error);
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
if (!response.errors || response.errors.length === 0) {
|
|
927
|
+
console.log('No errors');
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
for (const err of response.errors) {
|
|
931
|
+
console.log(`[ERROR] ${err.message}`);
|
|
932
|
+
if (err.stack)
|
|
933
|
+
console.log(err.stack);
|
|
934
|
+
if (err.url)
|
|
935
|
+
console.log(` at ${err.url}${err.line ? `:${err.line}` : ''}`);
|
|
936
|
+
console.log();
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
// ─── Network ─────────────────────────────────────────────────────────────────
|
|
940
|
+
browser
|
|
941
|
+
.command('requests <task>')
|
|
942
|
+
.description('Read captured network requests')
|
|
943
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
944
|
+
.option('-f, --filter <text>', 'Filter URLs containing text')
|
|
945
|
+
.option('--clear', 'Clear requests after reading')
|
|
946
|
+
.action(async (task, opts) => {
|
|
947
|
+
const response = await sendIPCRequest({
|
|
948
|
+
action: 'requests',
|
|
949
|
+
task,
|
|
950
|
+
tabId: opts.tab,
|
|
951
|
+
filter: opts.filter,
|
|
952
|
+
clear: opts.clear,
|
|
953
|
+
});
|
|
954
|
+
if (!response.ok) {
|
|
955
|
+
console.error(response.error);
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
if (!response.requests || response.requests.length === 0) {
|
|
959
|
+
console.log('No requests captured');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
console.log('METHOD'.padEnd(8) + 'STATUS'.padEnd(8) + 'URL');
|
|
963
|
+
console.log('-'.repeat(72));
|
|
964
|
+
for (const req of response.requests) {
|
|
965
|
+
const status = req.status ? String(req.status) : '...';
|
|
966
|
+
console.log(`${req.method.padEnd(8)}${status.padEnd(8)}${req.url.slice(0, 100)}`);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
browser
|
|
970
|
+
.command('responsebody <task> <url-pattern>')
|
|
971
|
+
.description('Wait for and read a response body by URL pattern')
|
|
972
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
973
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
974
|
+
.option('--max-chars <n>', 'Max characters to return', parseInt)
|
|
975
|
+
.action(async (task, urlPattern, opts) => {
|
|
976
|
+
const response = await sendIPCRequest({
|
|
977
|
+
action: 'response-body',
|
|
978
|
+
task,
|
|
979
|
+
tabId: opts.tab,
|
|
980
|
+
urlPattern,
|
|
981
|
+
timeout: opts.timeout,
|
|
982
|
+
maxChars: opts.maxChars,
|
|
983
|
+
});
|
|
984
|
+
if (!response.ok) {
|
|
985
|
+
console.error(response.error);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
console.log(response.body);
|
|
989
|
+
});
|
|
990
|
+
// ─── Wait ────────────────────────────────────────────────────────────────────
|
|
991
|
+
browser
|
|
992
|
+
.command('wait <task>')
|
|
993
|
+
.description('Wait for a condition')
|
|
994
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
995
|
+
.option('--time <ms>', 'Wait for milliseconds')
|
|
996
|
+
.option('--selector <css>', 'Wait for CSS selector to appear')
|
|
997
|
+
.option('--url <pattern>', 'Wait for URL to match pattern')
|
|
998
|
+
.option('--fn <js>', 'Wait for JS expression to return truthy')
|
|
999
|
+
.option('--state <state>', 'Wait for load state (domcontentloaded, load, networkidle)')
|
|
1000
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1001
|
+
.action(async (task, opts) => {
|
|
1002
|
+
let waitType;
|
|
1003
|
+
let waitValue;
|
|
1004
|
+
if (opts.time) {
|
|
1005
|
+
waitType = 'time';
|
|
1006
|
+
waitValue = parseInt(opts.time, 10);
|
|
1007
|
+
}
|
|
1008
|
+
else if (opts.selector) {
|
|
1009
|
+
waitType = 'selector';
|
|
1010
|
+
waitValue = opts.selector;
|
|
1011
|
+
}
|
|
1012
|
+
else if (opts.url) {
|
|
1013
|
+
waitType = 'url';
|
|
1014
|
+
waitValue = opts.url;
|
|
1015
|
+
}
|
|
1016
|
+
else if (opts.fn) {
|
|
1017
|
+
waitType = 'function';
|
|
1018
|
+
waitValue = opts.fn;
|
|
1019
|
+
}
|
|
1020
|
+
else if (opts.state) {
|
|
1021
|
+
waitType = 'load';
|
|
1022
|
+
waitValue = opts.state;
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
console.error('One of --time, --selector, --url, --fn, or --state required');
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
const response = await sendIPCRequest({
|
|
1029
|
+
action: 'wait',
|
|
1030
|
+
task,
|
|
1031
|
+
tabId: opts.tab,
|
|
1032
|
+
waitType,
|
|
1033
|
+
waitValue,
|
|
1034
|
+
timeout: opts.timeout,
|
|
1035
|
+
});
|
|
1036
|
+
if (!response.ok) {
|
|
1037
|
+
console.error(response.error);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
console.log('Wait condition met');
|
|
1041
|
+
});
|
|
1042
|
+
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
1043
|
+
browser
|
|
1044
|
+
.command('download <task>')
|
|
1045
|
+
.description('Set download directory for a task')
|
|
1046
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1047
|
+
.requiredOption('-p, --path <dir>', 'Download directory path')
|
|
1048
|
+
.action(async (task, opts) => {
|
|
1049
|
+
const response = await sendIPCRequest({
|
|
1050
|
+
action: 'set-download-path',
|
|
1051
|
+
task,
|
|
1052
|
+
tabId: opts.tab,
|
|
1053
|
+
downloadPath: opts.path,
|
|
1054
|
+
});
|
|
1055
|
+
if (!response.ok) {
|
|
1056
|
+
console.error(response.error);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
console.log(`Download path set to ${opts.path}`);
|
|
1060
|
+
});
|
|
1061
|
+
browser
|
|
1062
|
+
.command('waitdownload <task>')
|
|
1063
|
+
.description('Wait for a download to complete')
|
|
1064
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1065
|
+
.action(async (task, opts) => {
|
|
1066
|
+
const response = await sendIPCRequest({
|
|
1067
|
+
action: 'wait-download',
|
|
1068
|
+
task,
|
|
1069
|
+
timeout: opts.timeout,
|
|
1070
|
+
});
|
|
1071
|
+
if (!response.ok) {
|
|
1072
|
+
console.error(response.error);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
console.log(`Downloaded: ${response.downloadPath}`);
|
|
1076
|
+
});
|
|
378
1077
|
}
|
|
379
1078
|
function collect(val, memo) {
|
|
380
1079
|
memo.push(val);
|
|
@@ -390,3 +1089,14 @@ function formatAge(timestamp) {
|
|
|
390
1089
|
const hours = Math.floor(minutes / 60);
|
|
391
1090
|
return `${hours}h ago`;
|
|
392
1091
|
}
|
|
1092
|
+
function formatDuration(ms) {
|
|
1093
|
+
const seconds = Math.floor(ms / 1000);
|
|
1094
|
+
if (seconds < 60)
|
|
1095
|
+
return `${seconds}s`;
|
|
1096
|
+
const minutes = Math.floor(seconds / 60);
|
|
1097
|
+
if (minutes < 60)
|
|
1098
|
+
return `${minutes}m`;
|
|
1099
|
+
const hours = Math.floor(minutes / 60);
|
|
1100
|
+
const mm = minutes % 60;
|
|
1101
|
+
return mm ? `${hours}h ${mm}m` : `${hours}h`;
|
|
1102
|
+
}
|