codeloop-mcp-server 0.1.4 → 0.1.6
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/dist/auth/usage_tracker.d.ts +1 -1
- package/dist/auth/usage_tracker.d.ts.map +1 -1
- package/dist/auth/usage_tracker.js.map +1 -1
- package/dist/index.js +978 -40
- package/dist/index.js.map +1 -1
- package/dist/project-discovery.d.ts +17 -0
- package/dist/project-discovery.d.ts.map +1 -0
- package/dist/project-discovery.js +109 -0
- package/dist/project-discovery.js.map +1 -0
- package/dist/runners/app_logger.d.ts +41 -0
- package/dist/runners/app_logger.d.ts.map +1 -0
- package/dist/runners/app_logger.js +276 -0
- package/dist/runners/app_logger.js.map +1 -0
- package/dist/runners/base.d.ts.map +1 -1
- package/dist/runners/base.js +4 -2
- package/dist/runners/base.js.map +1 -1
- package/dist/runners/browser_interaction.d.ts +27 -0
- package/dist/runners/browser_interaction.d.ts.map +1 -0
- package/dist/runners/browser_interaction.js +294 -0
- package/dist/runners/browser_interaction.js.map +1 -0
- package/dist/runners/flutter.d.ts +1 -0
- package/dist/runners/flutter.d.ts.map +1 -1
- package/dist/runners/flutter.js +29 -0
- package/dist/runners/flutter.js.map +1 -1
- package/dist/runners/maestro_generator.d.ts +11 -0
- package/dist/runners/maestro_generator.d.ts.map +1 -0
- package/dist/runners/maestro_generator.js +79 -0
- package/dist/runners/maestro_generator.js.map +1 -0
- package/dist/runners/platform_detect.d.ts +14 -0
- package/dist/runners/platform_detect.d.ts.map +1 -0
- package/dist/runners/platform_detect.js +102 -0
- package/dist/runners/platform_detect.js.map +1 -0
- package/dist/runners/screenshot.d.ts +3 -7
- package/dist/runners/screenshot.d.ts.map +1 -1
- package/dist/runners/screenshot.js +155 -28
- package/dist/runners/screenshot.js.map +1 -1
- package/dist/runners/video_recorder.d.ts +49 -0
- package/dist/runners/video_recorder.d.ts.map +1 -0
- package/dist/runners/video_recorder.js +489 -0
- package/dist/runners/video_recorder.js.map +1 -0
- package/dist/runners/video_validator.d.ts +16 -0
- package/dist/runners/video_validator.d.ts.map +1 -0
- package/dist/runners/video_validator.js +123 -0
- package/dist/runners/video_validator.js.map +1 -0
- package/dist/runners/win_accessibility.d.ts +12 -0
- package/dist/runners/win_accessibility.d.ts.map +1 -0
- package/dist/runners/win_accessibility.js +101 -0
- package/dist/runners/win_accessibility.js.map +1 -0
- package/dist/runners/window_manager.d.ts +81 -0
- package/dist/runners/window_manager.d.ts.map +1 -0
- package/dist/runners/window_manager.js +1010 -0
- package/dist/runners/window_manager.js.map +1 -0
- package/dist/tools/design_compare.d.ts +1 -1
- package/dist/tools/design_compare.d.ts.map +1 -1
- package/dist/tools/design_compare.js +1 -2
- package/dist/tools/design_compare.js.map +1 -1
- package/dist/tools/discover_screens.d.ts +3 -3
- package/dist/tools/discover_screens.d.ts.map +1 -1
- package/dist/tools/discover_screens.js +140 -157
- package/dist/tools/discover_screens.js.map +1 -1
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +100 -5
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/init-project.d.ts +15 -0
- package/dist/tools/init-project.d.ts.map +1 -0
- package/dist/tools/init-project.js +273 -0
- package/dist/tools/init-project.js.map +1 -0
- package/dist/tools/interaction_replay.d.ts +8 -1
- package/dist/tools/interaction_replay.d.ts.map +1 -1
- package/dist/tools/interaction_replay.js +78 -2
- package/dist/tools/interaction_replay.js.map +1 -1
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +204 -53
- package/dist/tools/verify.js.map +1 -1
- package/dist/tools/visual_review.d.ts +1 -1
- package/dist/tools/visual_review.d.ts.map +1 -1
- package/dist/tools/visual_review.js +1 -2
- package/dist/tools/visual_review.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
import { platform, tmpdir } from "os";
|
|
2
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { runCommand, checkToolAvailable } from "./base.js";
|
|
5
|
+
/**
|
|
6
|
+
* Cross-platform window ID lookup.
|
|
7
|
+
* macOS: CGWindowListCopyWindowInfo via Swift
|
|
8
|
+
* Linux: xdotool search
|
|
9
|
+
* Windows: not needed (uses process handle)
|
|
10
|
+
*/
|
|
11
|
+
export async function findWindowId(appName) {
|
|
12
|
+
const os = platform();
|
|
13
|
+
if (os === "darwin")
|
|
14
|
+
return findMacOSWindowId(appName);
|
|
15
|
+
if (os === "linux")
|
|
16
|
+
return findLinuxWindowId(appName);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async function findMacOSWindowId(appName) {
|
|
20
|
+
const tmpFile = join(tmpdir(), `codeloop_findwindow_${Date.now()}.swift`);
|
|
21
|
+
const swiftCode = [
|
|
22
|
+
'import Cocoa',
|
|
23
|
+
'let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]',
|
|
24
|
+
'guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { exit(1) }',
|
|
25
|
+
`let search = "${appName}".lowercased()`,
|
|
26
|
+
'for window in windowList {',
|
|
27
|
+
' let owner = window[kCGWindowOwnerName as String] as? String ?? ""',
|
|
28
|
+
' let windowID = window[kCGWindowNumber as String] as? Int ?? 0',
|
|
29
|
+
' if owner.lowercased().contains(search) && windowID > 0 {',
|
|
30
|
+
' print(windowID)',
|
|
31
|
+
' exit(0)',
|
|
32
|
+
' }',
|
|
33
|
+
'}',
|
|
34
|
+
].join('\n');
|
|
35
|
+
writeFileSync(tmpFile, swiftCode);
|
|
36
|
+
try {
|
|
37
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
38
|
+
if (result.exit_code === 0 && result.stdout.trim()) {
|
|
39
|
+
return result.stdout.trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(tmpFile);
|
|
45
|
+
}
|
|
46
|
+
catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
async function findLinuxWindowId(appName) {
|
|
51
|
+
if (!(await checkToolAvailable("xdotool")))
|
|
52
|
+
return null;
|
|
53
|
+
const result = await runCommand("xdotool", ["search", "--name", appName], process.cwd());
|
|
54
|
+
if (result.exit_code === 0 && result.stdout.trim()) {
|
|
55
|
+
return result.stdout.trim().split("\n")[0];
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Bring an app to the front, un-minimizing if needed.
|
|
61
|
+
* Returns the name/title of the previously frontmost app for later restoration.
|
|
62
|
+
*/
|
|
63
|
+
export async function bringAppToFront(appName) {
|
|
64
|
+
const os = platform();
|
|
65
|
+
if (os === "darwin")
|
|
66
|
+
return bringToFrontMacOS(appName);
|
|
67
|
+
if (os === "win32")
|
|
68
|
+
return bringToFrontWindows(appName);
|
|
69
|
+
return bringToFrontLinux(appName);
|
|
70
|
+
}
|
|
71
|
+
async function bringToFrontMacOS(appName) {
|
|
72
|
+
const prevScript = 'tell application "System Events" to get name of first application process whose frontmost is true';
|
|
73
|
+
const prevResult = await runCommand("osascript", ["-e", prevScript], process.cwd());
|
|
74
|
+
let previousApp = prevResult.exit_code === 0 ? prevResult.stdout.trim() : "";
|
|
75
|
+
// If detection failed or returned the target app itself, find the IDE to restore later
|
|
76
|
+
if (!previousApp || previousApp.toLowerCase().includes(appName.toLowerCase())) {
|
|
77
|
+
previousApp = await detectRunningIDE();
|
|
78
|
+
}
|
|
79
|
+
const activateScript = `
|
|
80
|
+
tell application "System Events"
|
|
81
|
+
try
|
|
82
|
+
set targetProc to first process whose name contains "${appName}"
|
|
83
|
+
set visible of targetProc to true
|
|
84
|
+
end try
|
|
85
|
+
end tell
|
|
86
|
+
tell application "${appName}" to activate
|
|
87
|
+
`;
|
|
88
|
+
await runCommand("osascript", ["-e", activateScript], process.cwd());
|
|
89
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
90
|
+
return previousApp;
|
|
91
|
+
}
|
|
92
|
+
async function detectRunningIDE() {
|
|
93
|
+
const candidates = ["Cursor", "Code", "Visual Studio Code", "Terminal", "iTerm2", "Warp"];
|
|
94
|
+
for (const ide of candidates) {
|
|
95
|
+
const checkScript = `tell application "System Events" to (exists process "${ide}")`;
|
|
96
|
+
const result = await runCommand("osascript", ["-e", checkScript], process.cwd());
|
|
97
|
+
if (result.stdout.trim() === "true")
|
|
98
|
+
return ide;
|
|
99
|
+
}
|
|
100
|
+
return "Cursor";
|
|
101
|
+
}
|
|
102
|
+
async function bringToFrontWindows(appName) {
|
|
103
|
+
const script = `
|
|
104
|
+
Add-Type @"
|
|
105
|
+
using System;
|
|
106
|
+
using System.Runtime.InteropServices;
|
|
107
|
+
public class Win32Focus {
|
|
108
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
109
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
110
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
111
|
+
[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
|
|
112
|
+
}
|
|
113
|
+
"@
|
|
114
|
+
$prev = [Win32Focus]::GetForegroundWindow()
|
|
115
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
116
|
+
[Win32Focus]::GetWindowText($prev, $sb, 256)
|
|
117
|
+
Write-Output $sb.ToString()
|
|
118
|
+
$proc = Get-Process | Where-Object { $_.MainWindowTitle -match '${appName}' -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
119
|
+
if ($proc) {
|
|
120
|
+
[Win32Focus]::ShowWindow($proc.MainWindowHandle, 9)
|
|
121
|
+
[Win32Focus]::SetForegroundWindow($proc.MainWindowHandle)
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
125
|
+
return result.exit_code === 0 ? result.stdout.trim() : "";
|
|
126
|
+
}
|
|
127
|
+
async function bringToFrontLinux(appName) {
|
|
128
|
+
let previousWindow = "";
|
|
129
|
+
if (await checkToolAvailable("xdotool")) {
|
|
130
|
+
const activeResult = await runCommand("xdotool", ["getactivewindow", "getwindowname"], process.cwd());
|
|
131
|
+
previousWindow = activeResult.exit_code === 0 ? activeResult.stdout.trim() : "";
|
|
132
|
+
const searchResult = await runCommand("xdotool", ["search", "--name", appName], process.cwd());
|
|
133
|
+
if (searchResult.exit_code === 0 && searchResult.stdout.trim()) {
|
|
134
|
+
const windowId = searchResult.stdout.trim().split("\n")[0];
|
|
135
|
+
await runCommand("xdotool", ["windowactivate", windowId], process.cwd());
|
|
136
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return previousWindow;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the window bounds (position + size) for cropping video capture.
|
|
143
|
+
* Returns { x, y, width, height } in screen points.
|
|
144
|
+
*/
|
|
145
|
+
export async function getWindowBounds(appName) {
|
|
146
|
+
const os = platform();
|
|
147
|
+
if (os === "darwin")
|
|
148
|
+
return getWindowBoundsMacOS(appName);
|
|
149
|
+
if (os === "win32")
|
|
150
|
+
return getWindowBoundsWindows(appName);
|
|
151
|
+
if (os === "linux")
|
|
152
|
+
return getWindowBoundsLinux(appName);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
async function getWindowBoundsMacOS(appName) {
|
|
156
|
+
const tmpFile = join(tmpdir(), `codeloop_winbounds_${Date.now()}.swift`);
|
|
157
|
+
const swiftCode = [
|
|
158
|
+
'import Cocoa',
|
|
159
|
+
'let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]',
|
|
160
|
+
'guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { exit(1) }',
|
|
161
|
+
`let search = "${appName}".lowercased()`,
|
|
162
|
+
'for window in windowList {',
|
|
163
|
+
' let owner = window[kCGWindowOwnerName as String] as? String ?? ""',
|
|
164
|
+
' if owner.lowercased().contains(search) {',
|
|
165
|
+
' if let bounds = window[kCGWindowBounds as String] as? [String: Any] {',
|
|
166
|
+
' let x = bounds["X"] as? Int ?? 0',
|
|
167
|
+
' let y = bounds["Y"] as? Int ?? 0',
|
|
168
|
+
' let w = bounds["Width"] as? Int ?? 0',
|
|
169
|
+
' let h = bounds["Height"] as? Int ?? 0',
|
|
170
|
+
' if w > 50 && h > 50 {',
|
|
171
|
+
' print("\\(x),\\(y),\\(w),\\(h)")',
|
|
172
|
+
' exit(0)',
|
|
173
|
+
' }',
|
|
174
|
+
' }',
|
|
175
|
+
' }',
|
|
176
|
+
'}',
|
|
177
|
+
].join('\n');
|
|
178
|
+
writeFileSync(tmpFile, swiftCode);
|
|
179
|
+
try {
|
|
180
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
181
|
+
if (result.exit_code === 0 && result.stdout.trim()) {
|
|
182
|
+
const parts = result.stdout.trim().split(",").map(Number);
|
|
183
|
+
if (parts.length === 4 && parts.every(n => !isNaN(n))) {
|
|
184
|
+
return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
unlinkSync(tmpFile);
|
|
191
|
+
}
|
|
192
|
+
catch { /* ignore */ }
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async function getWindowBoundsWindows(appName) {
|
|
197
|
+
const script = `
|
|
198
|
+
Add-Type @"
|
|
199
|
+
using System;
|
|
200
|
+
using System.Runtime.InteropServices;
|
|
201
|
+
public class Win32Bounds {
|
|
202
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
203
|
+
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
204
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
205
|
+
}
|
|
206
|
+
"@
|
|
207
|
+
$proc = Get-Process | Where-Object { $_.MainWindowTitle -match '${appName}' -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
208
|
+
if ($proc) {
|
|
209
|
+
$rect = New-Object Win32Bounds+RECT
|
|
210
|
+
[Win32Bounds]::GetWindowRect($proc.MainWindowHandle, [ref]$rect) | Out-Null
|
|
211
|
+
Write-Output "$($rect.Left),$($rect.Top),$($rect.Right - $rect.Left),$($rect.Bottom - $rect.Top)"
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
215
|
+
if (result.exit_code === 0 && result.stdout.trim()) {
|
|
216
|
+
const parts = result.stdout.trim().split(",").map(Number);
|
|
217
|
+
if (parts.length === 4 && parts.every(n => !isNaN(n))) {
|
|
218
|
+
return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
async function getWindowBoundsLinux(appName) {
|
|
224
|
+
if (!(await checkToolAvailable("xdotool")))
|
|
225
|
+
return null;
|
|
226
|
+
const searchResult = await runCommand("xdotool", ["search", "--name", appName], process.cwd());
|
|
227
|
+
if (searchResult.exit_code !== 0 || !searchResult.stdout.trim())
|
|
228
|
+
return null;
|
|
229
|
+
const windowId = searchResult.stdout.trim().split("\n")[0];
|
|
230
|
+
const geoResult = await runCommand("xdotool", ["getwindowgeometry", "--shell", windowId], process.cwd());
|
|
231
|
+
if (geoResult.exit_code !== 0)
|
|
232
|
+
return null;
|
|
233
|
+
const lines = geoResult.stdout.split("\n");
|
|
234
|
+
let x = 0, y = 0, w = 0, h = 0;
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const [key, val] = line.split("=");
|
|
237
|
+
if (key === "X")
|
|
238
|
+
x = parseInt(val, 10);
|
|
239
|
+
else if (key === "Y")
|
|
240
|
+
y = parseInt(val, 10);
|
|
241
|
+
else if (key === "WIDTH")
|
|
242
|
+
w = parseInt(val, 10);
|
|
243
|
+
else if (key === "HEIGHT")
|
|
244
|
+
h = parseInt(val, 10);
|
|
245
|
+
}
|
|
246
|
+
if (w > 0 && h > 0)
|
|
247
|
+
return { x, y, width: w, height: h };
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Click at screen coordinates using CGEvent (macOS), user32.dll (Windows),
|
|
252
|
+
* or xdotool (Linux). These are low-level HID events that Flutter and all
|
|
253
|
+
* frameworks receive.
|
|
254
|
+
*/
|
|
255
|
+
export async function clickAtPosition(x, y) {
|
|
256
|
+
const os = platform();
|
|
257
|
+
if (os === "darwin") {
|
|
258
|
+
const tmpFile = join(tmpdir(), `codeloop_click_${Date.now()}.swift`);
|
|
259
|
+
const swiftCode = [
|
|
260
|
+
'import CoreGraphics',
|
|
261
|
+
'import Foundation',
|
|
262
|
+
`let point = CGPoint(x: ${x}, y: ${y})`,
|
|
263
|
+
'let mouseDown = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)',
|
|
264
|
+
'let mouseUp = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)',
|
|
265
|
+
'mouseDown?.post(tap: .cghidEventTap)',
|
|
266
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
267
|
+
'mouseUp?.post(tap: .cghidEventTap)',
|
|
268
|
+
].join('\n');
|
|
269
|
+
writeFileSync(tmpFile, swiftCode);
|
|
270
|
+
try {
|
|
271
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
272
|
+
return result.exit_code === 0;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
try {
|
|
276
|
+
unlinkSync(tmpFile);
|
|
277
|
+
}
|
|
278
|
+
catch { /* ignore */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (os === "win32") {
|
|
282
|
+
const script = `
|
|
283
|
+
Add-Type @"
|
|
284
|
+
using System;
|
|
285
|
+
using System.Runtime.InteropServices;
|
|
286
|
+
public class Win32Click {
|
|
287
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
288
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
289
|
+
}
|
|
290
|
+
"@
|
|
291
|
+
[Win32Click]::SetCursorPos(${x}, ${y})
|
|
292
|
+
Start-Sleep -Milliseconds 50
|
|
293
|
+
[Win32Click]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
294
|
+
Start-Sleep -Milliseconds 50
|
|
295
|
+
[Win32Click]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
296
|
+
`;
|
|
297
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
298
|
+
return result.exit_code === 0;
|
|
299
|
+
}
|
|
300
|
+
else if (os === "linux") {
|
|
301
|
+
if (await checkToolAvailable("xdotool")) {
|
|
302
|
+
const result = await runCommand("xdotool", ["mousemove", String(x), String(y), "click", "1"], process.cwd());
|
|
303
|
+
return result.exit_code === 0;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Send a keystroke using CGEvent (macOS), user32.dll keybd_event (Windows),
|
|
310
|
+
* or xdotool (Linux).
|
|
311
|
+
*
|
|
312
|
+
* keyCode values are platform-specific:
|
|
313
|
+
* - macOS: CGKeyCode (e.g., 36=Return, 53=Escape, 48=Tab)
|
|
314
|
+
* - Windows: Virtual Key code (e.g., 0x0D=Enter, 0x1B=Escape, 0x09=Tab)
|
|
315
|
+
* - Linux: X11 keysym name as string via keyName param, or keyCode ignored
|
|
316
|
+
*/
|
|
317
|
+
export async function sendKeystroke(keyCode, keyName) {
|
|
318
|
+
const os = platform();
|
|
319
|
+
if (os === "darwin") {
|
|
320
|
+
const tmpFile = join(tmpdir(), `codeloop_key_${Date.now()}.swift`);
|
|
321
|
+
const swiftCode = [
|
|
322
|
+
'import CoreGraphics',
|
|
323
|
+
'import Foundation',
|
|
324
|
+
`let keyDown = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(${keyCode}), keyDown: true)`,
|
|
325
|
+
`let keyUp = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(${keyCode}), keyDown: false)`,
|
|
326
|
+
'keyDown?.post(tap: .cghidEventTap)',
|
|
327
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
328
|
+
'keyUp?.post(tap: .cghidEventTap)',
|
|
329
|
+
].join('\n');
|
|
330
|
+
writeFileSync(tmpFile, swiftCode);
|
|
331
|
+
try {
|
|
332
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
333
|
+
return result.exit_code === 0;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
try {
|
|
337
|
+
unlinkSync(tmpFile);
|
|
338
|
+
}
|
|
339
|
+
catch { /* ignore */ }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (os === "win32") {
|
|
343
|
+
const vk = keyCode & 0xFF;
|
|
344
|
+
const script = `
|
|
345
|
+
Add-Type @"
|
|
346
|
+
using System;
|
|
347
|
+
using System.Runtime.InteropServices;
|
|
348
|
+
public class Win32Key {
|
|
349
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
|
350
|
+
}
|
|
351
|
+
"@
|
|
352
|
+
[Win32Key]::keybd_event(${vk}, 0, 0, [UIntPtr]::Zero)
|
|
353
|
+
Start-Sleep -Milliseconds 50
|
|
354
|
+
[Win32Key]::keybd_event(${vk}, 0, 2, [UIntPtr]::Zero)
|
|
355
|
+
`;
|
|
356
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
357
|
+
return result.exit_code === 0;
|
|
358
|
+
}
|
|
359
|
+
else if (os === "linux") {
|
|
360
|
+
if (await checkToolAvailable("xdotool")) {
|
|
361
|
+
const xKey = keyName || String(keyCode);
|
|
362
|
+
const result = await runCommand("xdotool", ["key", "--clearmodifiers", xKey], process.cwd());
|
|
363
|
+
return result.exit_code === 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
// ── Mobile Interaction (Android emulator via ADB) ───────────────
|
|
369
|
+
export async function adbTap(x, y) {
|
|
370
|
+
const result = await runCommand("adb", ["shell", "input", "tap", String(Math.round(x)), String(Math.round(y))], process.cwd());
|
|
371
|
+
return result.exit_code === 0;
|
|
372
|
+
}
|
|
373
|
+
export async function adbType(text) {
|
|
374
|
+
const escaped = text.replace(/ /g, "%s").replace(/'/g, "\\'");
|
|
375
|
+
const result = await runCommand("adb", ["shell", "input", "text", escaped], process.cwd());
|
|
376
|
+
return result.exit_code === 0;
|
|
377
|
+
}
|
|
378
|
+
export async function adbKey(keycode) {
|
|
379
|
+
const result = await runCommand("adb", ["shell", "input", "keyevent", keycode], process.cwd());
|
|
380
|
+
return result.exit_code === 0;
|
|
381
|
+
}
|
|
382
|
+
export async function adbSwipe(x1, y1, x2, y2, durationMs = 300) {
|
|
383
|
+
const result = await runCommand("adb", [
|
|
384
|
+
"shell", "input", "swipe",
|
|
385
|
+
String(Math.round(x1)), String(Math.round(y1)),
|
|
386
|
+
String(Math.round(x2)), String(Math.round(y2)),
|
|
387
|
+
String(durationMs),
|
|
388
|
+
], process.cwd());
|
|
389
|
+
return result.exit_code === 0;
|
|
390
|
+
}
|
|
391
|
+
// ── Mobile Interaction (iOS Simulator via simctl) ───────────────
|
|
392
|
+
export async function simctlLaunch(bundleId) {
|
|
393
|
+
const result = await runCommand("xcrun", ["simctl", "launch", "booted", bundleId], process.cwd());
|
|
394
|
+
return result.exit_code === 0;
|
|
395
|
+
}
|
|
396
|
+
export async function simctlTerminate(bundleId) {
|
|
397
|
+
const result = await runCommand("xcrun", ["simctl", "terminate", "booted", bundleId], process.cwd());
|
|
398
|
+
return result.exit_code === 0;
|
|
399
|
+
}
|
|
400
|
+
// ── Friendly Key Name Mapping ────────────────────────────────────
|
|
401
|
+
const KEY_MAP = {
|
|
402
|
+
enter: { mac: 36, win: 0x0D, linux: "Return" },
|
|
403
|
+
tab: { mac: 48, win: 0x09, linux: "Tab" },
|
|
404
|
+
escape: { mac: 53, win: 0x1B, linux: "Escape" },
|
|
405
|
+
backspace: { mac: 51, win: 0x08, linux: "BackSpace" },
|
|
406
|
+
delete: { mac: 117, win: 0x2E, linux: "Delete" },
|
|
407
|
+
space: { mac: 49, win: 0x20, linux: "space" },
|
|
408
|
+
up: { mac: 126, win: 0x26, linux: "Up" },
|
|
409
|
+
down: { mac: 125, win: 0x28, linux: "Down" },
|
|
410
|
+
left: { mac: 123, win: 0x25, linux: "Left" },
|
|
411
|
+
right: { mac: 124, win: 0x27, linux: "Right" },
|
|
412
|
+
home: { mac: 115, win: 0x24, linux: "Home" },
|
|
413
|
+
end: { mac: 119, win: 0x23, linux: "End" },
|
|
414
|
+
page_up: { mac: 116, win: 0x21, linux: "Prior" },
|
|
415
|
+
page_down: { mac: 121, win: 0x22, linux: "Next" },
|
|
416
|
+
f1: { mac: 122, win: 0x70, linux: "F1" },
|
|
417
|
+
f2: { mac: 120, win: 0x71, linux: "F2" },
|
|
418
|
+
f3: { mac: 99, win: 0x72, linux: "F3" },
|
|
419
|
+
f4: { mac: 118, win: 0x73, linux: "F4" },
|
|
420
|
+
f5: { mac: 96, win: 0x74, linux: "F5" },
|
|
421
|
+
f6: { mac: 97, win: 0x75, linux: "F6" },
|
|
422
|
+
f7: { mac: 98, win: 0x76, linux: "F7" },
|
|
423
|
+
f8: { mac: 100, win: 0x77, linux: "F8" },
|
|
424
|
+
f9: { mac: 101, win: 0x78, linux: "F9" },
|
|
425
|
+
f10: { mac: 109, win: 0x79, linux: "F10" },
|
|
426
|
+
f11: { mac: 103, win: 0x7A, linux: "F11" },
|
|
427
|
+
f12: { mac: 111, win: 0x7B, linux: "F12" },
|
|
428
|
+
};
|
|
429
|
+
export function getAvailableKeyNames() {
|
|
430
|
+
return Object.keys(KEY_MAP);
|
|
431
|
+
}
|
|
432
|
+
export async function sendKeyByName(keyName) {
|
|
433
|
+
const lower = keyName.toLowerCase();
|
|
434
|
+
const mapping = KEY_MAP[lower];
|
|
435
|
+
if (!mapping)
|
|
436
|
+
return false;
|
|
437
|
+
const os = platform();
|
|
438
|
+
if (os === "darwin")
|
|
439
|
+
return sendKeystroke(mapping.mac);
|
|
440
|
+
if (os === "win32")
|
|
441
|
+
return sendKeystroke(mapping.win);
|
|
442
|
+
return sendKeystroke(0, mapping.linux);
|
|
443
|
+
}
|
|
444
|
+
// ── Extended Desktop Interaction ────────────────────────────────
|
|
445
|
+
export async function typeText(text) {
|
|
446
|
+
const os = platform();
|
|
447
|
+
if (os === "darwin") {
|
|
448
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
449
|
+
const result = await runCommand("osascript", ["-e", `tell application "System Events" to keystroke "${escaped}"`], process.cwd());
|
|
450
|
+
return result.exit_code === 0;
|
|
451
|
+
}
|
|
452
|
+
else if (os === "win32") {
|
|
453
|
+
const escaped = text.replace(/[+^%~(){}[\]]/g, "{$&}");
|
|
454
|
+
const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait("${escaped}")`;
|
|
455
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
456
|
+
return result.exit_code === 0;
|
|
457
|
+
}
|
|
458
|
+
else if (os === "linux") {
|
|
459
|
+
if (await checkToolAvailable("xdotool")) {
|
|
460
|
+
const result = await runCommand("xdotool", ["type", "--clearmodifiers", text], process.cwd());
|
|
461
|
+
return result.exit_code === 0;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
export async function scrollAtPosition(x, y, direction, amount = 3) {
|
|
467
|
+
const os = platform();
|
|
468
|
+
if (os === "darwin") {
|
|
469
|
+
const deltaY = direction === "up" ? amount : direction === "down" ? -amount : 0;
|
|
470
|
+
const deltaX = direction === "left" ? amount : direction === "right" ? -amount : 0;
|
|
471
|
+
const tmpFile = join(tmpdir(), `codeloop_scroll_${Date.now()}.swift`);
|
|
472
|
+
const swiftCode = [
|
|
473
|
+
'import CoreGraphics',
|
|
474
|
+
'import Foundation',
|
|
475
|
+
`let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: CGPoint(x: ${x}, y: ${y}), mouseButton: .left)`,
|
|
476
|
+
'move?.post(tap: .cghidEventTap)',
|
|
477
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
478
|
+
`let scroll = CGEvent(scrollWheelEvent2Source: nil, units: .line, wheelCount: 2, wheel1: Int32(${deltaY}), wheel2: Int32(${deltaX}))`,
|
|
479
|
+
'scroll?.post(tap: .cghidEventTap)',
|
|
480
|
+
].join('\n');
|
|
481
|
+
writeFileSync(tmpFile, swiftCode);
|
|
482
|
+
try {
|
|
483
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
484
|
+
return result.exit_code === 0;
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
try {
|
|
488
|
+
unlinkSync(tmpFile);
|
|
489
|
+
}
|
|
490
|
+
catch { /* ignore */ }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (os === "win32") {
|
|
494
|
+
const wheelDelta = direction === "up" ? 120 * amount : direction === "down" ? -120 * amount : 0;
|
|
495
|
+
const script = `
|
|
496
|
+
Add-Type @"
|
|
497
|
+
using System;using System.Runtime.InteropServices;
|
|
498
|
+
public class Win32Scroll {
|
|
499
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
500
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
501
|
+
}
|
|
502
|
+
"@
|
|
503
|
+
[Win32Scroll]::SetCursorPos(${x}, ${y})
|
|
504
|
+
Start-Sleep -Milliseconds 50
|
|
505
|
+
[Win32Scroll]::mouse_event(0x0800, 0, 0, ${wheelDelta}, [UIntPtr]::Zero)`;
|
|
506
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
507
|
+
return result.exit_code === 0;
|
|
508
|
+
}
|
|
509
|
+
else if (os === "linux") {
|
|
510
|
+
if (await checkToolAvailable("xdotool")) {
|
|
511
|
+
await runCommand("xdotool", ["mousemove", String(x), String(y)], process.cwd());
|
|
512
|
+
const button = direction === "up" ? "4" : direction === "down" ? "5" : direction === "left" ? "6" : "7";
|
|
513
|
+
for (let i = 0; i < amount; i++) {
|
|
514
|
+
await runCommand("xdotool", ["click", button], process.cwd());
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
export async function doubleClickAtPosition(x, y) {
|
|
522
|
+
const os = platform();
|
|
523
|
+
if (os === "darwin") {
|
|
524
|
+
const tmpFile = join(tmpdir(), `codeloop_dblclick_${Date.now()}.swift`);
|
|
525
|
+
const swiftCode = [
|
|
526
|
+
'import CoreGraphics',
|
|
527
|
+
'import Foundation',
|
|
528
|
+
`let point = CGPoint(x: ${x}, y: ${y})`,
|
|
529
|
+
'let d1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)',
|
|
530
|
+
'd1?.setIntegerValueField(.mouseEventClickState, value: 1)',
|
|
531
|
+
'd1?.post(tap: .cghidEventTap)',
|
|
532
|
+
'let u1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)',
|
|
533
|
+
'u1?.setIntegerValueField(.mouseEventClickState, value: 1)',
|
|
534
|
+
'u1?.post(tap: .cghidEventTap)',
|
|
535
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
536
|
+
'let d2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)',
|
|
537
|
+
'd2?.setIntegerValueField(.mouseEventClickState, value: 2)',
|
|
538
|
+
'd2?.post(tap: .cghidEventTap)',
|
|
539
|
+
'let u2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)',
|
|
540
|
+
'u2?.setIntegerValueField(.mouseEventClickState, value: 2)',
|
|
541
|
+
'u2?.post(tap: .cghidEventTap)',
|
|
542
|
+
].join('\n');
|
|
543
|
+
writeFileSync(tmpFile, swiftCode);
|
|
544
|
+
try {
|
|
545
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
546
|
+
return result.exit_code === 0;
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
try {
|
|
550
|
+
unlinkSync(tmpFile);
|
|
551
|
+
}
|
|
552
|
+
catch { /* ignore */ }
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (os === "win32") {
|
|
556
|
+
const script = `
|
|
557
|
+
Add-Type @"
|
|
558
|
+
using System;using System.Runtime.InteropServices;
|
|
559
|
+
public class Win32DblClick {
|
|
560
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
561
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
562
|
+
}
|
|
563
|
+
"@
|
|
564
|
+
[Win32DblClick]::SetCursorPos(${x}, ${y})
|
|
565
|
+
[Win32DblClick]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
566
|
+
[Win32DblClick]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
567
|
+
Start-Sleep -Milliseconds 50
|
|
568
|
+
[Win32DblClick]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
569
|
+
[Win32DblClick]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)`;
|
|
570
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
571
|
+
return result.exit_code === 0;
|
|
572
|
+
}
|
|
573
|
+
else if (os === "linux") {
|
|
574
|
+
if (await checkToolAvailable("xdotool")) {
|
|
575
|
+
const result = await runCommand("xdotool", ["mousemove", String(x), String(y), "click", "--repeat", "2", "1"], process.cwd());
|
|
576
|
+
return result.exit_code === 0;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
export async function rightClickAtPosition(x, y) {
|
|
582
|
+
const os = platform();
|
|
583
|
+
if (os === "darwin") {
|
|
584
|
+
const tmpFile = join(tmpdir(), `codeloop_rclick_${Date.now()}.swift`);
|
|
585
|
+
const swiftCode = [
|
|
586
|
+
'import CoreGraphics',
|
|
587
|
+
'import Foundation',
|
|
588
|
+
`let point = CGPoint(x: ${x}, y: ${y})`,
|
|
589
|
+
'let down = CGEvent(mouseEventSource: nil, mouseType: .rightMouseDown, mouseCursorPosition: point, mouseButton: .right)',
|
|
590
|
+
'let up = CGEvent(mouseEventSource: nil, mouseType: .rightMouseUp, mouseCursorPosition: point, mouseButton: .right)',
|
|
591
|
+
'down?.post(tap: .cghidEventTap)',
|
|
592
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
593
|
+
'up?.post(tap: .cghidEventTap)',
|
|
594
|
+
].join('\n');
|
|
595
|
+
writeFileSync(tmpFile, swiftCode);
|
|
596
|
+
try {
|
|
597
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
598
|
+
return result.exit_code === 0;
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
try {
|
|
602
|
+
unlinkSync(tmpFile);
|
|
603
|
+
}
|
|
604
|
+
catch { /* ignore */ }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (os === "win32") {
|
|
608
|
+
const script = `
|
|
609
|
+
Add-Type @"
|
|
610
|
+
using System;using System.Runtime.InteropServices;
|
|
611
|
+
public class Win32RClick {
|
|
612
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
613
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
614
|
+
}
|
|
615
|
+
"@
|
|
616
|
+
[Win32RClick]::SetCursorPos(${x}, ${y})
|
|
617
|
+
Start-Sleep -Milliseconds 50
|
|
618
|
+
[Win32RClick]::mouse_event(0x0008, 0, 0, 0, [UIntPtr]::Zero)
|
|
619
|
+
Start-Sleep -Milliseconds 50
|
|
620
|
+
[Win32RClick]::mouse_event(0x0010, 0, 0, 0, [UIntPtr]::Zero)`;
|
|
621
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
622
|
+
return result.exit_code === 0;
|
|
623
|
+
}
|
|
624
|
+
else if (os === "linux") {
|
|
625
|
+
if (await checkToolAvailable("xdotool")) {
|
|
626
|
+
const result = await runCommand("xdotool", ["mousemove", String(x), String(y), "click", "3"], process.cwd());
|
|
627
|
+
return result.exit_code === 0;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
export async function hoverAtPosition(x, y) {
|
|
633
|
+
const os = platform();
|
|
634
|
+
if (os === "darwin") {
|
|
635
|
+
const tmpFile = join(tmpdir(), `codeloop_hover_${Date.now()}.swift`);
|
|
636
|
+
const swiftCode = [
|
|
637
|
+
'import CoreGraphics',
|
|
638
|
+
'import Foundation',
|
|
639
|
+
`let event = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: CGPoint(x: ${x}, y: ${y}), mouseButton: .left)`,
|
|
640
|
+
'event?.post(tap: .cghidEventTap)',
|
|
641
|
+
].join('\n');
|
|
642
|
+
writeFileSync(tmpFile, swiftCode);
|
|
643
|
+
try {
|
|
644
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
645
|
+
return result.exit_code === 0;
|
|
646
|
+
}
|
|
647
|
+
finally {
|
|
648
|
+
try {
|
|
649
|
+
unlinkSync(tmpFile);
|
|
650
|
+
}
|
|
651
|
+
catch { /* ignore */ }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else if (os === "win32") {
|
|
655
|
+
const script = `
|
|
656
|
+
Add-Type @"
|
|
657
|
+
using System;using System.Runtime.InteropServices;
|
|
658
|
+
public class Win32Hover { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); }
|
|
659
|
+
"@
|
|
660
|
+
[Win32Hover]::SetCursorPos(${x}, ${y})`;
|
|
661
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
662
|
+
return result.exit_code === 0;
|
|
663
|
+
}
|
|
664
|
+
else if (os === "linux") {
|
|
665
|
+
if (await checkToolAvailable("xdotool")) {
|
|
666
|
+
const result = await runCommand("xdotool", ["mousemove", String(x), String(y)], process.cwd());
|
|
667
|
+
return result.exit_code === 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
const MODIFIER_KEYCODES = {
|
|
673
|
+
cmd: { mac: 55, win: 0x5B, linux: "super", macFlag: "kCGEventFlagMaskCommand" },
|
|
674
|
+
meta: { mac: 55, win: 0x5B, linux: "super", macFlag: "kCGEventFlagMaskCommand" },
|
|
675
|
+
ctrl: { mac: 59, win: 0x11, linux: "ctrl", macFlag: "kCGEventFlagMaskControl" },
|
|
676
|
+
alt: { mac: 58, win: 0x12, linux: "alt", macFlag: "kCGEventFlagMaskAlternate" },
|
|
677
|
+
shift: { mac: 56, win: 0x10, linux: "shift", macFlag: "kCGEventFlagMaskShift" },
|
|
678
|
+
};
|
|
679
|
+
export async function sendHotkey(combo) {
|
|
680
|
+
const parts = combo.toLowerCase().split("+").map(s => s.trim());
|
|
681
|
+
const key = parts.pop();
|
|
682
|
+
const modifiers = parts;
|
|
683
|
+
const os = platform();
|
|
684
|
+
if (os === "linux" && (await checkToolAvailable("xdotool"))) {
|
|
685
|
+
const xdoCombo = [...modifiers, KEY_MAP[key]?.linux || key].join("+");
|
|
686
|
+
const result = await runCommand("xdotool", ["key", "--clearmodifiers", xdoCombo], process.cwd());
|
|
687
|
+
return result.exit_code === 0;
|
|
688
|
+
}
|
|
689
|
+
if (os === "darwin") {
|
|
690
|
+
const keyMapping = KEY_MAP[key];
|
|
691
|
+
const keyCode = keyMapping ? keyMapping.mac : key.charCodeAt(0) - (key >= 'a' ? 97 - 0 : 65 - 0);
|
|
692
|
+
const flags = modifiers.map(m => MODIFIER_KEYCODES[m]?.macFlag).filter(Boolean);
|
|
693
|
+
const flagExpr = flags.length > 0
|
|
694
|
+
? `var flags: CGEventFlags = []; ${flags.map(f => `flags.insert(.${f})`).join("; ")}`
|
|
695
|
+
: "let flags: CGEventFlags = []";
|
|
696
|
+
const tmpFile = join(tmpdir(), `codeloop_hotkey_${Date.now()}.swift`);
|
|
697
|
+
const swiftCode = [
|
|
698
|
+
'import CoreGraphics',
|
|
699
|
+
'import Foundation',
|
|
700
|
+
flagExpr,
|
|
701
|
+
`let keyDown = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(${keyCode}), keyDown: true)`,
|
|
702
|
+
`let keyUp = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(${keyCode}), keyDown: false)`,
|
|
703
|
+
'keyDown?.flags = flags',
|
|
704
|
+
'keyUp?.flags = flags',
|
|
705
|
+
'keyDown?.post(tap: .cghidEventTap)',
|
|
706
|
+
'Thread.sleep(forTimeInterval: 0.05)',
|
|
707
|
+
'keyUp?.post(tap: .cghidEventTap)',
|
|
708
|
+
].join('\n');
|
|
709
|
+
writeFileSync(tmpFile, swiftCode);
|
|
710
|
+
try {
|
|
711
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
712
|
+
return result.exit_code === 0;
|
|
713
|
+
}
|
|
714
|
+
finally {
|
|
715
|
+
try {
|
|
716
|
+
unlinkSync(tmpFile);
|
|
717
|
+
}
|
|
718
|
+
catch { /* ignore */ }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (os === "win32") {
|
|
722
|
+
const vkKey = KEY_MAP[key]?.win || key.toUpperCase().charCodeAt(0);
|
|
723
|
+
const modVks = modifiers.map(m => MODIFIER_KEYCODES[m]?.win).filter(Boolean);
|
|
724
|
+
const downStatements = modVks.map(vk => `[Win32HK]::keybd_event(${vk}, 0, 0, [UIntPtr]::Zero)`).join("; ");
|
|
725
|
+
const upStatements = modVks.map(vk => `[Win32HK]::keybd_event(${vk}, 0, 2, [UIntPtr]::Zero)`).join("; ");
|
|
726
|
+
const script = `
|
|
727
|
+
Add-Type @"
|
|
728
|
+
using System;using System.Runtime.InteropServices;
|
|
729
|
+
public class Win32HK { [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); }
|
|
730
|
+
"@
|
|
731
|
+
${downStatements}
|
|
732
|
+
[Win32HK]::keybd_event(${vkKey}, 0, 0, [UIntPtr]::Zero)
|
|
733
|
+
Start-Sleep -Milliseconds 50
|
|
734
|
+
[Win32HK]::keybd_event(${vkKey}, 0, 2, [UIntPtr]::Zero)
|
|
735
|
+
${upStatements}`;
|
|
736
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
737
|
+
return result.exit_code === 0;
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
export async function dragDrop(x1, y1, x2, y2, durationMs = 500) {
|
|
742
|
+
const os = platform();
|
|
743
|
+
if (os === "darwin") {
|
|
744
|
+
const steps = 20;
|
|
745
|
+
const tmpFile = join(tmpdir(), `codeloop_drag_${Date.now()}.swift`);
|
|
746
|
+
const swiftCode = [
|
|
747
|
+
'import CoreGraphics',
|
|
748
|
+
'import Foundation',
|
|
749
|
+
`let start = CGPoint(x: ${x1}, y: ${y1})`,
|
|
750
|
+
`let end = CGPoint(x: ${x2}, y: ${y2})`,
|
|
751
|
+
'let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: start, mouseButton: .left)',
|
|
752
|
+
'down?.post(tap: .cghidEventTap)',
|
|
753
|
+
`for i in 1...${steps} {`,
|
|
754
|
+
` let t = Double(i) / ${steps}.0`,
|
|
755
|
+
' let x = start.x + (end.x - start.x) * t',
|
|
756
|
+
' let y = start.y + (end.y - start.y) * t',
|
|
757
|
+
' let drag = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: CGPoint(x: x, y: y), mouseButton: .left)',
|
|
758
|
+
' drag?.post(tap: .cghidEventTap)',
|
|
759
|
+
` Thread.sleep(forTimeInterval: ${durationMs / 1000.0 / steps})`,
|
|
760
|
+
'}',
|
|
761
|
+
'let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: end, mouseButton: .left)',
|
|
762
|
+
'up?.post(tap: .cghidEventTap)',
|
|
763
|
+
].join('\n');
|
|
764
|
+
writeFileSync(tmpFile, swiftCode);
|
|
765
|
+
try {
|
|
766
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
767
|
+
return result.exit_code === 0;
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
try {
|
|
771
|
+
unlinkSync(tmpFile);
|
|
772
|
+
}
|
|
773
|
+
catch { /* ignore */ }
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
else if (os === "win32") {
|
|
777
|
+
const script = `
|
|
778
|
+
Add-Type @"
|
|
779
|
+
using System;using System.Runtime.InteropServices;
|
|
780
|
+
public class Win32Drag {
|
|
781
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
782
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
783
|
+
}
|
|
784
|
+
"@
|
|
785
|
+
[Win32Drag]::SetCursorPos(${x1}, ${y1}); Start-Sleep -Milliseconds 50
|
|
786
|
+
[Win32Drag]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
787
|
+
$steps = 20; for ($i = 1; $i -le $steps; $i++) { $t = $i / $steps; $cx = [int](${x1} + (${x2} - ${x1}) * $t); $cy = [int](${y1} + (${y2} - ${y1}) * $t); [Win32Drag]::SetCursorPos($cx, $cy); Start-Sleep -Milliseconds ${Math.round(durationMs / 20)} }
|
|
788
|
+
[Win32Drag]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)`;
|
|
789
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
790
|
+
return result.exit_code === 0;
|
|
791
|
+
}
|
|
792
|
+
else if (os === "linux") {
|
|
793
|
+
if (await checkToolAvailable("xdotool")) {
|
|
794
|
+
const delay = Math.round(durationMs / 20);
|
|
795
|
+
const result = await runCommand("xdotool", [
|
|
796
|
+
"mousemove", String(x1), String(y1), "mousedown", "1",
|
|
797
|
+
"mousemove", "--delay", String(delay), String(x2), String(y2),
|
|
798
|
+
"mouseup", "1",
|
|
799
|
+
], process.cwd());
|
|
800
|
+
return result.exit_code === 0;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
export async function longPressAtPosition(x, y, durationMs = 1000) {
|
|
806
|
+
const os = platform();
|
|
807
|
+
if (os === "darwin") {
|
|
808
|
+
const tmpFile = join(tmpdir(), `codeloop_longpress_${Date.now()}.swift`);
|
|
809
|
+
const swiftCode = [
|
|
810
|
+
'import CoreGraphics',
|
|
811
|
+
'import Foundation',
|
|
812
|
+
`let point = CGPoint(x: ${x}, y: ${y})`,
|
|
813
|
+
'let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)',
|
|
814
|
+
'down?.post(tap: .cghidEventTap)',
|
|
815
|
+
`Thread.sleep(forTimeInterval: ${durationMs / 1000.0})`,
|
|
816
|
+
'let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)',
|
|
817
|
+
'up?.post(tap: .cghidEventTap)',
|
|
818
|
+
].join('\n');
|
|
819
|
+
writeFileSync(tmpFile, swiftCode);
|
|
820
|
+
try {
|
|
821
|
+
const result = await runCommand("swift", [tmpFile], process.cwd());
|
|
822
|
+
return result.exit_code === 0;
|
|
823
|
+
}
|
|
824
|
+
finally {
|
|
825
|
+
try {
|
|
826
|
+
unlinkSync(tmpFile);
|
|
827
|
+
}
|
|
828
|
+
catch { /* ignore */ }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else if (os === "win32") {
|
|
832
|
+
const script = `
|
|
833
|
+
Add-Type @"
|
|
834
|
+
using System;using System.Runtime.InteropServices;
|
|
835
|
+
public class Win32LP {
|
|
836
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
837
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
|
|
838
|
+
}
|
|
839
|
+
"@
|
|
840
|
+
[Win32LP]::SetCursorPos(${x}, ${y}); Start-Sleep -Milliseconds 50
|
|
841
|
+
[Win32LP]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
842
|
+
Start-Sleep -Milliseconds ${durationMs}
|
|
843
|
+
[Win32LP]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)`;
|
|
844
|
+
const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
845
|
+
return result.exit_code === 0;
|
|
846
|
+
}
|
|
847
|
+
else if (os === "linux") {
|
|
848
|
+
if (await checkToolAvailable("xdotool")) {
|
|
849
|
+
await runCommand("xdotool", ["mousemove", String(x), String(y), "mousedown", "1"], process.cwd());
|
|
850
|
+
await new Promise(r => setTimeout(r, durationMs));
|
|
851
|
+
const result = await runCommand("xdotool", ["mouseup", "1"], process.cwd());
|
|
852
|
+
return result.exit_code === 0;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
// ── iOS Simulator Extended Interaction ──────────────────────────
|
|
858
|
+
export async function simctlTap(x, y) {
|
|
859
|
+
const result = await runCommand("xcrun", ["simctl", "io", "booted", "tap", String(Math.round(x)), String(Math.round(y))], process.cwd());
|
|
860
|
+
return result.exit_code === 0;
|
|
861
|
+
}
|
|
862
|
+
export async function simctlType(text) {
|
|
863
|
+
for (const char of text) {
|
|
864
|
+
const result = await runCommand("xcrun", ["simctl", "io", "booted", "type", char], process.cwd());
|
|
865
|
+
if (result.exit_code !== 0)
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
export async function simctlOpenUrl(url) {
|
|
871
|
+
const result = await runCommand("xcrun", ["simctl", "openurl", "booted", url], process.cwd());
|
|
872
|
+
return result.exit_code === 0;
|
|
873
|
+
}
|
|
874
|
+
export async function simctlBiometric(accept) {
|
|
875
|
+
await runCommand("xcrun", ["simctl", "biometric", "booted", "--state", "enrolled"], process.cwd());
|
|
876
|
+
const action = accept ? "--match" : "--no-match";
|
|
877
|
+
const result = await runCommand("xcrun", ["simctl", "biometric", "booted", action], process.cwd());
|
|
878
|
+
return result.exit_code === 0;
|
|
879
|
+
}
|
|
880
|
+
// ── Extended Android (adb) Helpers ──────────────────────────────
|
|
881
|
+
export async function adbDeepLink(url) {
|
|
882
|
+
const result = await runCommand("adb", ["shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url], process.cwd());
|
|
883
|
+
return result.exit_code === 0;
|
|
884
|
+
}
|
|
885
|
+
export async function adbPermission(packageId, permission, grant) {
|
|
886
|
+
const action = grant ? "grant" : "revoke";
|
|
887
|
+
const result = await runCommand("adb", ["shell", "pm", action, packageId, permission], process.cwd());
|
|
888
|
+
return result.exit_code === 0;
|
|
889
|
+
}
|
|
890
|
+
export async function adbHomeButton() {
|
|
891
|
+
return adbKey("KEYCODE_HOME");
|
|
892
|
+
}
|
|
893
|
+
export async function adbBackButton() {
|
|
894
|
+
return adbKey("KEYCODE_BACK");
|
|
895
|
+
}
|
|
896
|
+
export async function adbRecentApps() {
|
|
897
|
+
return adbKey("KEYCODE_APP_SWITCH");
|
|
898
|
+
}
|
|
899
|
+
export async function adbNotificationShade(expand) {
|
|
900
|
+
const action = expand ? "expand-notifications" : "collapse";
|
|
901
|
+
const result = await runCommand("adb", ["shell", "cmd", "statusbar", action], process.cwd());
|
|
902
|
+
return result.exit_code === 0;
|
|
903
|
+
}
|
|
904
|
+
export async function adbLongPress(x, y, durationMs = 1000) {
|
|
905
|
+
return adbSwipe(x, y, x, y, durationMs);
|
|
906
|
+
}
|
|
907
|
+
export async function adbClearData(packageId) {
|
|
908
|
+
const result = await runCommand("adb", ["shell", "pm", "clear", packageId], process.cwd());
|
|
909
|
+
return result.exit_code === 0;
|
|
910
|
+
}
|
|
911
|
+
export async function adbInstallApk(apkPath) {
|
|
912
|
+
const result = await runCommand("adb", ["install", "-r", apkPath], process.cwd());
|
|
913
|
+
return result.exit_code === 0;
|
|
914
|
+
}
|
|
915
|
+
export async function adbNetworkCondition(mode) {
|
|
916
|
+
const result = await runCommand("adb", ["emu", "network", "speed", mode], process.cwd());
|
|
917
|
+
return result.exit_code === 0;
|
|
918
|
+
}
|
|
919
|
+
export async function adbMockLocation(lat, lng) {
|
|
920
|
+
const result = await runCommand("adb", ["emu", "geo", "fix", String(lng), String(lat)], process.cwd());
|
|
921
|
+
return result.exit_code === 0;
|
|
922
|
+
}
|
|
923
|
+
export async function adbRotate(landscape) {
|
|
924
|
+
await runCommand("adb", ["shell", "settings", "put", "system", "accelerometer_rotation", "0"], process.cwd());
|
|
925
|
+
const rotation = landscape ? "1" : "0";
|
|
926
|
+
const result = await runCommand("adb", ["shell", "settings", "put", "system", "user_rotation", rotation], process.cwd());
|
|
927
|
+
return result.exit_code === 0;
|
|
928
|
+
}
|
|
929
|
+
const INSTALL_INSTRUCTIONS = {
|
|
930
|
+
ffmpeg: {
|
|
931
|
+
macOS: "brew install ffmpeg",
|
|
932
|
+
windows: "winget install ffmpeg (or: choco install ffmpeg)",
|
|
933
|
+
linux: "sudo apt-get install -y ffmpeg (or: sudo dnf install -y ffmpeg)",
|
|
934
|
+
},
|
|
935
|
+
adb: {
|
|
936
|
+
macOS: "brew install android-platform-tools",
|
|
937
|
+
windows: "winget install Google.PlatformTools (or install via Android Studio)",
|
|
938
|
+
linux: "sudo apt-get install -y android-tools-adb",
|
|
939
|
+
},
|
|
940
|
+
xcrun: {
|
|
941
|
+
macOS: "xcode-select --install",
|
|
942
|
+
windows: "(not available on Windows — iOS development requires macOS)",
|
|
943
|
+
linux: "(not available on Linux — iOS development requires macOS)",
|
|
944
|
+
},
|
|
945
|
+
xdotool: {
|
|
946
|
+
macOS: "(not needed on macOS)",
|
|
947
|
+
windows: "(not needed on Windows)",
|
|
948
|
+
linux: "sudo apt-get install -y xdotool",
|
|
949
|
+
},
|
|
950
|
+
maestro: {
|
|
951
|
+
macOS: 'curl -Ls "https://get.maestro.mobile.dev" | bash',
|
|
952
|
+
windows: 'curl -Ls "https://get.maestro.mobile.dev" | bash',
|
|
953
|
+
linux: 'curl -Ls "https://get.maestro.mobile.dev" | bash',
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
/**
|
|
957
|
+
* Check if a required external tool is installed.
|
|
958
|
+
* Returns null if available, or a user-facing error message with install instructions.
|
|
959
|
+
*/
|
|
960
|
+
export async function checkDependency(tool, action) {
|
|
961
|
+
const os = platform();
|
|
962
|
+
const whichCmd = os === "win32" ? "where" : "which";
|
|
963
|
+
const result = await runCommand(whichCmd, [tool], process.cwd());
|
|
964
|
+
if (result.exit_code === 0 && result.stdout.trim())
|
|
965
|
+
return null;
|
|
966
|
+
const instructions = INSTALL_INSTRUCTIONS[tool];
|
|
967
|
+
if (!instructions) {
|
|
968
|
+
return `Tool "${tool}" is not installed but is required for ${action}. Please install it and retry.`;
|
|
969
|
+
}
|
|
970
|
+
const platformKey = os === "darwin" ? "macOS" : os === "win32" ? "windows" : "linux";
|
|
971
|
+
const installCmd = instructions[platformKey];
|
|
972
|
+
return `Tool "${tool}" is not installed but is required for ${action}.\n\nInstall it with:\n ${installCmd}\n\nThen retry this operation.`;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Restore the previously frontmost app/window after capture is complete.
|
|
976
|
+
*/
|
|
977
|
+
export async function restorePreviousApp(previousApp) {
|
|
978
|
+
const os = platform();
|
|
979
|
+
// If no previous app was captured, try to detect and restore the IDE
|
|
980
|
+
if (!previousApp && os === "darwin") {
|
|
981
|
+
previousApp = await detectRunningIDE();
|
|
982
|
+
}
|
|
983
|
+
if (!previousApp)
|
|
984
|
+
return;
|
|
985
|
+
if (os === "darwin") {
|
|
986
|
+
await runCommand("osascript", ["-e", `tell application "${previousApp}" to activate`], process.cwd());
|
|
987
|
+
}
|
|
988
|
+
else if (os === "win32") {
|
|
989
|
+
const script = `
|
|
990
|
+
$proc = Get-Process | Where-Object { $_.MainWindowTitle -match '${previousApp}' -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
|
991
|
+
if ($proc) {
|
|
992
|
+
Add-Type @"
|
|
993
|
+
using System;using System.Runtime.InteropServices;
|
|
994
|
+
public class Win32R { [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); }
|
|
995
|
+
"@
|
|
996
|
+
[Win32R]::SetForegroundWindow($proc.MainWindowHandle)
|
|
997
|
+
}`;
|
|
998
|
+
await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
if (await checkToolAvailable("xdotool")) {
|
|
1002
|
+
const searchResult = await runCommand("xdotool", ["search", "--name", previousApp], process.cwd());
|
|
1003
|
+
if (searchResult.exit_code === 0 && searchResult.stdout.trim()) {
|
|
1004
|
+
const windowId = searchResult.stdout.trim().split("\n")[0];
|
|
1005
|
+
await runCommand("xdotool", ["windowactivate", windowId], process.cwd());
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
//# sourceMappingURL=window_manager.js.map
|