@web-auto/camo 0.1.2
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 -0
- package/README.md +243 -0
- package/bin/camo.mjs +22 -0
- package/package.json +39 -0
- package/scripts/build.mjs +19 -0
- package/scripts/bump-version.mjs +38 -0
- package/scripts/install.mjs +76 -0
- package/scripts/release.sh +54 -0
- package/src/cli.mjs +199 -0
- package/src/commands/browser.mjs +462 -0
- package/src/commands/cookies.mjs +69 -0
- package/src/commands/create.mjs +98 -0
- package/src/commands/init.mjs +68 -0
- package/src/commands/lifecycle.mjs +256 -0
- package/src/commands/mouse.mjs +49 -0
- package/src/commands/profile.mjs +46 -0
- package/src/commands/system.mjs +14 -0
- package/src/commands/window.mjs +31 -0
- package/src/lifecycle/cleanup.mjs +83 -0
- package/src/lifecycle/lock.mjs +122 -0
- package/src/lifecycle/session-registry.mjs +163 -0
- package/src/utils/args.mjs +25 -0
- package/src/utils/browser-service.mjs +194 -0
- package/src/utils/config.mjs +90 -0
- package/src/utils/fingerprint.mjs +181 -0
- package/src/utils/help.mjs +128 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { listProfiles, getDefaultProfile, loadConfig, hasStartScript, setRepoRoot } from './utils/config.mjs';
|
|
5
|
+
import { ensureCamoufox, ensureBrowserService } from './utils/browser-service.mjs';
|
|
6
|
+
import { printHelp, printProfilesAndHint } from './utils/help.mjs';
|
|
7
|
+
import { handleProfileCommand } from './commands/profile.mjs';
|
|
8
|
+
import { handleInitCommand } from './commands/init.mjs';
|
|
9
|
+
import { handleCreateCommand } from './commands/create.mjs';
|
|
10
|
+
import { handleCookiesCommand } from './commands/cookies.mjs';
|
|
11
|
+
import { handleWindowCommand } from './commands/window.mjs';
|
|
12
|
+
import { handleMouseCommand } from './commands/mouse.mjs';
|
|
13
|
+
import { handleSystemCommand } from './commands/system.mjs';
|
|
14
|
+
import {
|
|
15
|
+
handleStartCommand, handleStopCommand, handleStatusCommand,
|
|
16
|
+
handleGotoCommand, handleBackCommand, handleScreenshotCommand,
|
|
17
|
+
handleScrollCommand, handleClickCommand, handleTypeCommand,
|
|
18
|
+
handleHighlightCommand, handleClearHighlightCommand, handleViewportCommand,
|
|
19
|
+
handleNewPageCommand, handleClosePageCommand, handleSwitchPageCommand,
|
|
20
|
+
handleListPagesCommand, handleShutdownCommand
|
|
21
|
+
} from './commands/browser.mjs';
|
|
22
|
+
import {
|
|
23
|
+
handleCleanupCommand, handleForceStopCommand, handleLockCommand,
|
|
24
|
+
handleUnlockCommand, handleSessionsCommand
|
|
25
|
+
} from './commands/lifecycle.mjs';
|
|
26
|
+
|
|
27
|
+
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
|
|
29
|
+
|
|
30
|
+
async function handleConfigCommand(args) {
|
|
31
|
+
const sub = args[1];
|
|
32
|
+
if (sub !== 'repo-root') {
|
|
33
|
+
throw new Error('Usage: camo config repo-root [path]');
|
|
34
|
+
}
|
|
35
|
+
const repoRoot = args[2];
|
|
36
|
+
if (!repoRoot) {
|
|
37
|
+
console.log(JSON.stringify({ ok: true, repoRoot: loadConfig().repoRoot }, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const resolved = path.resolve(repoRoot);
|
|
41
|
+
if (!hasStartScript(resolved)) {
|
|
42
|
+
throw new Error(`Invalid repo root: ${resolved} (missing ${START_SCRIPT_REL})`);
|
|
43
|
+
}
|
|
44
|
+
setRepoRoot(resolved);
|
|
45
|
+
console.log(JSON.stringify({ ok: true, repoRoot: resolved }, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const cmd = args[0];
|
|
51
|
+
|
|
52
|
+
if (!cmd) {
|
|
53
|
+
printProfilesAndHint(listProfiles, getDefaultProfile);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
58
|
+
printHelp();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (cmd === 'profiles') {
|
|
63
|
+
const profiles = listProfiles();
|
|
64
|
+
const defaultProfile = getDefaultProfile();
|
|
65
|
+
console.log(JSON.stringify({ ok: true, profiles, defaultProfile, count: profiles.length }, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (cmd === 'profile') {
|
|
70
|
+
await handleProfileCommand(args);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (cmd === 'config') {
|
|
75
|
+
await handleConfigCommand(args);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === 'init') {
|
|
80
|
+
await handleInitCommand(args);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cmd === 'create') {
|
|
85
|
+
await handleCreateCommand(args);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Lifecycle commands
|
|
90
|
+
if (cmd === 'cleanup') {
|
|
91
|
+
await handleCleanupCommand(args);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cmd === 'force-stop') {
|
|
96
|
+
await handleForceStopCommand(args);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cmd === 'lock') {
|
|
101
|
+
await handleLockCommand(args);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cmd === 'unlock') {
|
|
106
|
+
await handleUnlockCommand(args);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (cmd === 'sessions') {
|
|
111
|
+
await handleSessionsCommand(args);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const serviceCommands = new Set([
|
|
116
|
+
'start', 'stop', 'close', 'status', 'list', 'goto', 'navigate', 'back', 'screenshot',
|
|
117
|
+
'new-page', 'close-page', 'switch-page', 'list-pages', 'shutdown',
|
|
118
|
+
'scroll', 'click', 'type', 'highlight', 'clear-highlight', 'viewport',
|
|
119
|
+
'cookies', 'window', 'mouse', 'system',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
if (!serviceCommands.has(cmd)) {
|
|
123
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (cmd) {
|
|
127
|
+
case 'start':
|
|
128
|
+
await handleStartCommand(args);
|
|
129
|
+
break;
|
|
130
|
+
case 'stop':
|
|
131
|
+
case 'close':
|
|
132
|
+
await handleStopCommand(args);
|
|
133
|
+
break;
|
|
134
|
+
case 'status':
|
|
135
|
+
case 'list':
|
|
136
|
+
await handleStatusCommand(args);
|
|
137
|
+
break;
|
|
138
|
+
case 'goto':
|
|
139
|
+
case 'navigate':
|
|
140
|
+
await handleGotoCommand(args);
|
|
141
|
+
break;
|
|
142
|
+
case 'back':
|
|
143
|
+
await handleBackCommand(args);
|
|
144
|
+
break;
|
|
145
|
+
case 'screenshot':
|
|
146
|
+
await handleScreenshotCommand(args);
|
|
147
|
+
break;
|
|
148
|
+
case 'scroll':
|
|
149
|
+
await handleScrollCommand(args);
|
|
150
|
+
break;
|
|
151
|
+
case 'click':
|
|
152
|
+
await handleClickCommand(args);
|
|
153
|
+
break;
|
|
154
|
+
case 'type':
|
|
155
|
+
await handleTypeCommand(args);
|
|
156
|
+
break;
|
|
157
|
+
case 'highlight':
|
|
158
|
+
await handleHighlightCommand(args);
|
|
159
|
+
break;
|
|
160
|
+
case 'clear-highlight':
|
|
161
|
+
await handleClearHighlightCommand(args);
|
|
162
|
+
break;
|
|
163
|
+
case 'viewport':
|
|
164
|
+
await handleViewportCommand(args);
|
|
165
|
+
break;
|
|
166
|
+
case 'new-page':
|
|
167
|
+
await handleNewPageCommand(args);
|
|
168
|
+
break;
|
|
169
|
+
case 'close-page':
|
|
170
|
+
await handleClosePageCommand(args);
|
|
171
|
+
break;
|
|
172
|
+
case 'switch-page':
|
|
173
|
+
await handleSwitchPageCommand(args);
|
|
174
|
+
break;
|
|
175
|
+
case 'list-pages':
|
|
176
|
+
await handleListPagesCommand(args);
|
|
177
|
+
break;
|
|
178
|
+
case 'shutdown':
|
|
179
|
+
await handleShutdownCommand();
|
|
180
|
+
break;
|
|
181
|
+
case 'cookies':
|
|
182
|
+
await handleCookiesCommand(args);
|
|
183
|
+
break;
|
|
184
|
+
case 'window':
|
|
185
|
+
await handleWindowCommand(args);
|
|
186
|
+
break;
|
|
187
|
+
case 'mouse':
|
|
188
|
+
await handleMouseCommand(args);
|
|
189
|
+
break;
|
|
190
|
+
case 'system':
|
|
191
|
+
await handleSystemCommand(args);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main().catch((err) => {
|
|
197
|
+
console.error(`Error: ${err?.message || String(err)}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { listProfiles, getDefaultProfile } from '../utils/config.mjs';
|
|
3
|
+
import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
|
|
4
|
+
import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
|
|
5
|
+
import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
6
|
+
import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
|
|
7
|
+
|
|
8
|
+
export async function handleStartCommand(args) {
|
|
9
|
+
ensureCamoufox();
|
|
10
|
+
await ensureBrowserService();
|
|
11
|
+
cleanupStaleLocks();
|
|
12
|
+
cleanupStaleSessions();
|
|
13
|
+
|
|
14
|
+
const urlIdx = args.indexOf('--url');
|
|
15
|
+
const explicitUrl = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
|
|
16
|
+
const profileSet = new Set(listProfiles());
|
|
17
|
+
let implicitUrl;
|
|
18
|
+
|
|
19
|
+
let profileId = null;
|
|
20
|
+
for (let i = 1; i < args.length; i++) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (arg === '--url') { i++; continue; }
|
|
23
|
+
if (arg === '--headless') continue;
|
|
24
|
+
if (arg.startsWith('--')) continue;
|
|
25
|
+
|
|
26
|
+
if (looksLikeUrlToken(arg) && !profileSet.has(arg)) {
|
|
27
|
+
implicitUrl = arg;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
profileId = arg;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!profileId) {
|
|
36
|
+
profileId = getDefaultProfile();
|
|
37
|
+
if (!profileId) {
|
|
38
|
+
throw new Error('No default profile set. Run: camo profile default <profileId>');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for existing session in browser service
|
|
43
|
+
const existing = await getSessionByProfile(profileId);
|
|
44
|
+
if (existing) {
|
|
45
|
+
// Session exists in browser service - update registry and lock
|
|
46
|
+
acquireLock(profileId, { sessionId: existing.session_id || existing.profileId });
|
|
47
|
+
registerSession(profileId, {
|
|
48
|
+
sessionId: existing.session_id || existing.profileId,
|
|
49
|
+
url: existing.current_url,
|
|
50
|
+
mode: existing.mode,
|
|
51
|
+
});
|
|
52
|
+
console.log(JSON.stringify({
|
|
53
|
+
ok: true,
|
|
54
|
+
sessionId: existing.session_id || existing.profileId,
|
|
55
|
+
profileId,
|
|
56
|
+
message: 'Session already running',
|
|
57
|
+
url: existing.current_url,
|
|
58
|
+
}, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// No session in browser service - check registry for recovery
|
|
63
|
+
const registryInfo = getSessionInfo(profileId);
|
|
64
|
+
if (registryInfo && registryInfo.status === 'active') {
|
|
65
|
+
// Session was active but browser service doesn't have it
|
|
66
|
+
// This means service was restarted - clean up and start fresh
|
|
67
|
+
unregisterSession(profileId);
|
|
68
|
+
releaseLock(profileId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const headless = args.includes('--headless');
|
|
72
|
+
const targetUrl = explicitUrl || implicitUrl;
|
|
73
|
+
const result = await callAPI('start', {
|
|
74
|
+
profileId,
|
|
75
|
+
url: targetUrl ? ensureUrlScheme(targetUrl) : undefined,
|
|
76
|
+
headless,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (result?.ok) {
|
|
80
|
+
const sessionId = result.sessionId || result.profileId || profileId;
|
|
81
|
+
acquireLock(profileId, { sessionId });
|
|
82
|
+
registerSession(profileId, {
|
|
83
|
+
sessionId,
|
|
84
|
+
url: targetUrl,
|
|
85
|
+
headless,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
console.log(JSON.stringify(result, null, 2));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function handleStopCommand(args) {
|
|
92
|
+
await ensureBrowserService();
|
|
93
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
94
|
+
if (!profileId) throw new Error('Usage: camo stop [profileId]');
|
|
95
|
+
|
|
96
|
+
const result = await callAPI('stop', { profileId });
|
|
97
|
+
releaseLock(profileId);
|
|
98
|
+
markSessionClosed(profileId);
|
|
99
|
+
console.log(JSON.stringify(result, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function handleStatusCommand(args) {
|
|
103
|
+
await ensureBrowserService();
|
|
104
|
+
const result = await callAPI('getStatus', {});
|
|
105
|
+
const profileId = args[1];
|
|
106
|
+
if (profileId && args[0] === 'status') {
|
|
107
|
+
const session = result?.sessions?.find((s) => s.profileId === profileId) || null;
|
|
108
|
+
console.log(JSON.stringify({ ok: true, session }, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(JSON.stringify(result, null, 2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function handleGotoCommand(args) {
|
|
115
|
+
await ensureBrowserService();
|
|
116
|
+
const positionals = getPositionals(args);
|
|
117
|
+
|
|
118
|
+
let profileId;
|
|
119
|
+
let url;
|
|
120
|
+
|
|
121
|
+
if (positionals.length === 1) {
|
|
122
|
+
profileId = getDefaultProfile();
|
|
123
|
+
url = positionals[0];
|
|
124
|
+
} else {
|
|
125
|
+
profileId = resolveProfileId(positionals, 0, getDefaultProfile);
|
|
126
|
+
url = positionals[1];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!profileId) throw new Error('Usage: camo goto [profileId] <url> (or set default profile first)');
|
|
130
|
+
if (!url) throw new Error('Usage: camo goto [profileId] <url>');
|
|
131
|
+
|
|
132
|
+
const result = await callAPI('goto', { profileId, url: ensureUrlScheme(url) });
|
|
133
|
+
updateSession(profileId, { url: ensureUrlScheme(url), lastSeen: Date.now() });
|
|
134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function handleBackCommand(args) {
|
|
138
|
+
await ensureBrowserService();
|
|
139
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
140
|
+
if (!profileId) throw new Error('Usage: camo back [profileId] (or set default profile first)');
|
|
141
|
+
const result = await callAPI('page:back', { profileId });
|
|
142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function handleScreenshotCommand(args) {
|
|
146
|
+
await ensureBrowserService();
|
|
147
|
+
const fullPage = args.includes('--full');
|
|
148
|
+
const outputIdx = args.indexOf('--output');
|
|
149
|
+
const output = outputIdx >= 0 ? args[outputIdx + 1] : null;
|
|
150
|
+
|
|
151
|
+
let profileId = null;
|
|
152
|
+
for (let i = 1; i < args.length; i++) {
|
|
153
|
+
const arg = args[i];
|
|
154
|
+
if (arg === '--full') continue;
|
|
155
|
+
if (arg === '--output') { i++; continue; }
|
|
156
|
+
if (arg.startsWith('--')) continue;
|
|
157
|
+
profileId = arg;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!profileId) profileId = getDefaultProfile();
|
|
162
|
+
if (!profileId) throw new Error('Usage: camo screenshot [profileId] [--output <file>] [--full]');
|
|
163
|
+
|
|
164
|
+
const result = await callAPI('screenshot', { profileId, fullPage });
|
|
165
|
+
|
|
166
|
+
if (output && result?.data) {
|
|
167
|
+
fs.writeFileSync(output, Buffer.from(result.data, 'base64'));
|
|
168
|
+
console.log(`Screenshot saved to ${output}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function handleScrollCommand(args) {
|
|
176
|
+
await ensureBrowserService();
|
|
177
|
+
const directionFlags = new Set(['--up', '--down', '--left', '--right']);
|
|
178
|
+
const isFlag = (arg) => arg?.startsWith('--');
|
|
179
|
+
|
|
180
|
+
let profileId = null;
|
|
181
|
+
for (let i = 1; i < args.length; i++) {
|
|
182
|
+
const arg = args[i];
|
|
183
|
+
if (directionFlags.has(arg)) continue;
|
|
184
|
+
if (arg === '--amount') { i++; continue; }
|
|
185
|
+
if (isFlag(arg)) continue;
|
|
186
|
+
profileId = arg;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (!profileId) profileId = getDefaultProfile();
|
|
190
|
+
if (!profileId) throw new Error('Usage: camo scroll [profileId] [--down|--up|--left|--right] [--amount <px>]');
|
|
191
|
+
|
|
192
|
+
const direction = args.includes('--up') ? 'up' : args.includes('--left') ? 'left' : args.includes('--right') ? 'right' : 'down';
|
|
193
|
+
const amountIdx = args.indexOf('--amount');
|
|
194
|
+
const amount = amountIdx >= 0 ? Number(args[amountIdx + 1]) || 300 : 300;
|
|
195
|
+
|
|
196
|
+
const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
197
|
+
const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
198
|
+
const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function handleClickCommand(args) {
|
|
203
|
+
await ensureBrowserService();
|
|
204
|
+
const positionals = getPositionals(args);
|
|
205
|
+
let profileId;
|
|
206
|
+
let selector;
|
|
207
|
+
|
|
208
|
+
if (positionals.length === 1) {
|
|
209
|
+
profileId = getDefaultProfile();
|
|
210
|
+
selector = positionals[0];
|
|
211
|
+
} else {
|
|
212
|
+
profileId = positionals[0];
|
|
213
|
+
selector = positionals[1];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!profileId) throw new Error('Usage: camo click [profileId] <selector>');
|
|
217
|
+
if (!selector) throw new Error('Usage: camo click [profileId] <selector>');
|
|
218
|
+
|
|
219
|
+
const result = await callAPI('evaluate', {
|
|
220
|
+
profileId,
|
|
221
|
+
script: `(async () => {
|
|
222
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
223
|
+
if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
|
|
224
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
225
|
+
await new Promise(r => setTimeout(r, 200));
|
|
226
|
+
el.click();
|
|
227
|
+
return { clicked: true, selector: ${JSON.stringify(selector)} };
|
|
228
|
+
})()`
|
|
229
|
+
});
|
|
230
|
+
console.log(JSON.stringify(result, null, 2));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function handleTypeCommand(args) {
|
|
234
|
+
await ensureBrowserService();
|
|
235
|
+
const positionals = getPositionals(args);
|
|
236
|
+
let profileId;
|
|
237
|
+
let selector;
|
|
238
|
+
let text;
|
|
239
|
+
|
|
240
|
+
if (positionals.length === 2) {
|
|
241
|
+
profileId = getDefaultProfile();
|
|
242
|
+
selector = positionals[0];
|
|
243
|
+
text = positionals[1];
|
|
244
|
+
} else {
|
|
245
|
+
profileId = positionals[0];
|
|
246
|
+
selector = positionals[1];
|
|
247
|
+
text = positionals[2];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!profileId) throw new Error('Usage: camo type [profileId] <selector> <text>');
|
|
251
|
+
if (!selector || text === undefined) throw new Error('Usage: camo type [profileId] <selector> <text>');
|
|
252
|
+
|
|
253
|
+
const result = await callAPI('evaluate', {
|
|
254
|
+
profileId,
|
|
255
|
+
script: `(async () => {
|
|
256
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
257
|
+
if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
|
|
258
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
259
|
+
await new Promise(r => setTimeout(r, 200));
|
|
260
|
+
el.focus();
|
|
261
|
+
el.value = '';
|
|
262
|
+
el.value = ${JSON.stringify(text)};
|
|
263
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
264
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
265
|
+
return { typed: true, selector: ${JSON.stringify(selector)}, length: ${text.length} };
|
|
266
|
+
})()`
|
|
267
|
+
});
|
|
268
|
+
console.log(JSON.stringify(result, null, 2));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function handleHighlightCommand(args) {
|
|
272
|
+
await ensureBrowserService();
|
|
273
|
+
const positionals = getPositionals(args);
|
|
274
|
+
let profileId;
|
|
275
|
+
let selector;
|
|
276
|
+
|
|
277
|
+
if (positionals.length === 1) {
|
|
278
|
+
profileId = getDefaultProfile();
|
|
279
|
+
selector = positionals[0];
|
|
280
|
+
} else {
|
|
281
|
+
profileId = positionals[0];
|
|
282
|
+
selector = positionals[1];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!profileId) throw new Error('Usage: camo highlight [profileId] <selector>');
|
|
286
|
+
if (!selector) throw new Error('Usage: camo highlight [profileId] <selector>');
|
|
287
|
+
|
|
288
|
+
const result = await callAPI('evaluate', {
|
|
289
|
+
profileId,
|
|
290
|
+
script: `(() => {
|
|
291
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
292
|
+
if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
|
|
293
|
+
const prev = el.style.outline;
|
|
294
|
+
el.style.outline = '3px solid #ff4444';
|
|
295
|
+
setTimeout(() => { el.style.outline = prev; }, 2000);
|
|
296
|
+
const rect = el.getBoundingClientRect();
|
|
297
|
+
return { highlighted: true, selector: ${JSON.stringify(selector)}, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
298
|
+
})()`
|
|
299
|
+
});
|
|
300
|
+
console.log(JSON.stringify(result, null, 2));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function handleClearHighlightCommand(args) {
|
|
304
|
+
await ensureBrowserService();
|
|
305
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
306
|
+
if (!profileId) throw new Error('Usage: camo clear-highlight [profileId]');
|
|
307
|
+
|
|
308
|
+
const result = await callAPI('evaluate', {
|
|
309
|
+
profileId,
|
|
310
|
+
script: `(() => {
|
|
311
|
+
const overlay = document.getElementById('webauto-highlight-overlay');
|
|
312
|
+
if (overlay) overlay.remove();
|
|
313
|
+
document.querySelectorAll('[data-webauto-highlight]').forEach(el => {
|
|
314
|
+
el.style.outline = el.dataset.webautoHighlight || '';
|
|
315
|
+
delete el.dataset.webautoHighlight;
|
|
316
|
+
});
|
|
317
|
+
return { cleared: true };
|
|
318
|
+
})()`
|
|
319
|
+
});
|
|
320
|
+
console.log(JSON.stringify(result, null, 2));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function handleViewportCommand(args) {
|
|
324
|
+
await ensureBrowserService();
|
|
325
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
326
|
+
if (!profileId) throw new Error('Usage: camo viewport [profileId] --width <w> --height <h>');
|
|
327
|
+
|
|
328
|
+
const widthIdx = args.indexOf('--width');
|
|
329
|
+
const heightIdx = args.indexOf('--height');
|
|
330
|
+
const width = widthIdx >= 0 ? Number(args[widthIdx + 1]) : 1280;
|
|
331
|
+
const height = heightIdx >= 0 ? Number(args[heightIdx + 1]) : 800;
|
|
332
|
+
|
|
333
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
334
|
+
throw new Error('Usage: camo viewport [profileId] --width <w> --height <h>');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = await callAPI('page:setViewport', { profileId, width, height });
|
|
338
|
+
console.log(JSON.stringify(result, null, 2));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function handleNewPageCommand(args) {
|
|
342
|
+
await ensureBrowserService();
|
|
343
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
344
|
+
if (!profileId) throw new Error('Usage: camo new-page [profileId] [--url <url>] (or set default profile first)');
|
|
345
|
+
const urlIdx = args.indexOf('--url');
|
|
346
|
+
const url = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
|
|
347
|
+
const result = await callAPI('newPage', { profileId, ...(url ? { url: ensureUrlScheme(url) } : {}) });
|
|
348
|
+
console.log(JSON.stringify(result, null, 2));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function handleClosePageCommand(args) {
|
|
352
|
+
await ensureBrowserService();
|
|
353
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
354
|
+
if (!profileId) throw new Error('Usage: camo close-page [profileId] [index] (or set default profile first)');
|
|
355
|
+
|
|
356
|
+
let index;
|
|
357
|
+
for (let i = args.length - 1; i >= 1; i--) {
|
|
358
|
+
const arg = args[i];
|
|
359
|
+
if (arg.startsWith('--')) continue;
|
|
360
|
+
const num = Number(arg);
|
|
361
|
+
if (Number.isFinite(num)) { index = num; break; }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const result = await callAPI('page:close', { profileId, ...(Number.isFinite(index) ? { index } : {}) });
|
|
365
|
+
console.log(JSON.stringify(result, null, 2));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function handleSwitchPageCommand(args) {
|
|
369
|
+
await ensureBrowserService();
|
|
370
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
371
|
+
if (!profileId) throw new Error('Usage: camo switch-page [profileId] <index> (or set default profile first)');
|
|
372
|
+
|
|
373
|
+
let index;
|
|
374
|
+
for (let i = args.length - 1; i >= 1; i--) {
|
|
375
|
+
const arg = args[i];
|
|
376
|
+
if (arg.startsWith('--')) continue;
|
|
377
|
+
const num = Number(arg);
|
|
378
|
+
if (Number.isFinite(num)) { index = num; break; }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!Number.isFinite(index)) throw new Error('Usage: camo switch-page [profileId] <index>');
|
|
382
|
+
const result = await callAPI('page:switch', { profileId, index });
|
|
383
|
+
console.log(JSON.stringify(result, null, 2));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function handleListPagesCommand(args) {
|
|
387
|
+
await ensureBrowserService();
|
|
388
|
+
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
389
|
+
if (!profileId) throw new Error('Usage: camo list-pages [profileId] (or set default profile first)');
|
|
390
|
+
const result = await callAPI('page:list', { profileId });
|
|
391
|
+
console.log(JSON.stringify(result, null, 2));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function handleShutdownCommand() {
|
|
395
|
+
await ensureBrowserService();
|
|
396
|
+
|
|
397
|
+
// Get all active sessions
|
|
398
|
+
const status = await callAPI('getStatus', {});
|
|
399
|
+
const sessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
400
|
+
|
|
401
|
+
// Stop each session and cleanup registry
|
|
402
|
+
for (const session of sessions) {
|
|
403
|
+
try {
|
|
404
|
+
await callAPI('stop', { profileId: session.profileId });
|
|
405
|
+
} catch {
|
|
406
|
+
// Best effort cleanup
|
|
407
|
+
}
|
|
408
|
+
releaseLock(session.profileId);
|
|
409
|
+
markSessionClosed(session.profileId);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Cleanup any remaining registry entries
|
|
413
|
+
const registered = listRegisteredSessions();
|
|
414
|
+
for (const reg of registered) {
|
|
415
|
+
if (reg.status !== 'closed') {
|
|
416
|
+
markSessionClosed(reg.profileId);
|
|
417
|
+
releaseLock(reg.profileId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const result = await callAPI('service:shutdown', {});
|
|
422
|
+
console.log(JSON.stringify(result, null, 2));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function handleSessionsCommand(args) {
|
|
426
|
+
const serviceUp = await checkBrowserService();
|
|
427
|
+
const registeredSessions = listRegisteredSessions();
|
|
428
|
+
|
|
429
|
+
let liveSessions = [];
|
|
430
|
+
if (serviceUp) {
|
|
431
|
+
try {
|
|
432
|
+
const status = await callAPI('getStatus', {});
|
|
433
|
+
liveSessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
434
|
+
} catch {
|
|
435
|
+
// Service may have just become unavailable
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Merge live and registered sessions
|
|
440
|
+
const liveProfileIds = new Set(liveSessions.map(s => s.profileId));
|
|
441
|
+
const merged = [...liveSessions];
|
|
442
|
+
|
|
443
|
+
// Add registered sessions that are not in live sessions (need recovery)
|
|
444
|
+
for (const reg of registeredSessions) {
|
|
445
|
+
if (!liveProfileIds.has(reg.profileId) && reg.status === 'active') {
|
|
446
|
+
merged.push({
|
|
447
|
+
...reg,
|
|
448
|
+
live: false,
|
|
449
|
+
needsRecovery: true,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log(JSON.stringify({
|
|
455
|
+
ok: true,
|
|
456
|
+
serviceUp,
|
|
457
|
+
sessions: merged,
|
|
458
|
+
count: merged.length,
|
|
459
|
+
registered: registeredSessions.length,
|
|
460
|
+
live: liveSessions.length,
|
|
461
|
+
}, null, 2));
|
|
462
|
+
}
|