copilot-liku-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +411 -0
- package/CONFIGURATION.md +302 -0
- package/CONTRIBUTING.md +225 -0
- package/ELECTRON_README.md +121 -0
- package/INSTALLATION.md +350 -0
- package/LICENSE.md +1 -0
- package/PROJECT_STATUS.md +229 -0
- package/QUICKSTART.md +255 -0
- package/README.md +167 -0
- package/TESTING.md +274 -0
- package/package.json +61 -0
- package/scripts/start.js +30 -0
- package/src/assets/tray-icon.png +0 -0
- package/src/cli/commands/agent.js +327 -0
- package/src/cli/commands/click.js +108 -0
- package/src/cli/commands/drag.js +85 -0
- package/src/cli/commands/find.js +109 -0
- package/src/cli/commands/keys.js +132 -0
- package/src/cli/commands/mouse.js +79 -0
- package/src/cli/commands/repl.js +290 -0
- package/src/cli/commands/screenshot.js +72 -0
- package/src/cli/commands/scroll.js +74 -0
- package/src/cli/commands/start.js +67 -0
- package/src/cli/commands/type.js +57 -0
- package/src/cli/commands/wait.js +84 -0
- package/src/cli/commands/window.js +104 -0
- package/src/cli/liku.js +249 -0
- package/src/cli/util/output.js +174 -0
- package/src/main/agents/base-agent.js +410 -0
- package/src/main/agents/builder.js +484 -0
- package/src/main/agents/index.js +62 -0
- package/src/main/agents/orchestrator.js +362 -0
- package/src/main/agents/researcher.js +511 -0
- package/src/main/agents/state-manager.js +344 -0
- package/src/main/agents/supervisor.js +365 -0
- package/src/main/agents/verifier.js +452 -0
- package/src/main/ai-service.js +1633 -0
- package/src/main/index.js +2208 -0
- package/src/main/inspect-service.js +467 -0
- package/src/main/system-automation.js +1186 -0
- package/src/main/ui-automation/config.js +76 -0
- package/src/main/ui-automation/core/helpers.js +41 -0
- package/src/main/ui-automation/core/index.js +15 -0
- package/src/main/ui-automation/core/powershell.js +82 -0
- package/src/main/ui-automation/elements/finder.js +274 -0
- package/src/main/ui-automation/elements/index.js +14 -0
- package/src/main/ui-automation/elements/wait.js +66 -0
- package/src/main/ui-automation/index.js +164 -0
- package/src/main/ui-automation/interactions/element-click.js +211 -0
- package/src/main/ui-automation/interactions/high-level.js +230 -0
- package/src/main/ui-automation/interactions/index.js +47 -0
- package/src/main/ui-automation/keyboard/index.js +15 -0
- package/src/main/ui-automation/keyboard/input.js +179 -0
- package/src/main/ui-automation/mouse/click.js +186 -0
- package/src/main/ui-automation/mouse/drag.js +88 -0
- package/src/main/ui-automation/mouse/index.js +30 -0
- package/src/main/ui-automation/mouse/movement.js +51 -0
- package/src/main/ui-automation/mouse/scroll.js +116 -0
- package/src/main/ui-automation/screenshot.js +183 -0
- package/src/main/ui-automation/window/index.js +23 -0
- package/src/main/ui-automation/window/manager.js +305 -0
- package/src/main/utils/time.js +62 -0
- package/src/main/visual-awareness.js +597 -0
- package/src/renderer/chat/chat.js +671 -0
- package/src/renderer/chat/index.html +725 -0
- package/src/renderer/chat/preload.js +112 -0
- package/src/renderer/overlay/index.html +648 -0
- package/src/renderer/overlay/overlay.js +782 -0
- package/src/renderer/overlay/preload.js +90 -0
- package/src/shared/grid-math.js +82 -0
- package/src/shared/inspect-types.js +230 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Automation Configuration
|
|
3
|
+
*
|
|
4
|
+
* Central configuration for the UI automation module.
|
|
5
|
+
* @module ui-automation/config
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONFIGURATION
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
// Default timeouts (ms)
|
|
18
|
+
DEFAULT_TIMEOUT: 10000,
|
|
19
|
+
ELEMENT_WAIT_INTERVAL: 100,
|
|
20
|
+
CLICK_DELAY: 50,
|
|
21
|
+
FOCUS_DELAY: 100,
|
|
22
|
+
|
|
23
|
+
// PowerShell execution
|
|
24
|
+
PS_MAX_BUFFER: 10 * 1024 * 1024,
|
|
25
|
+
|
|
26
|
+
// Temp directory for scripts
|
|
27
|
+
TEMP_DIR: path.join(os.tmpdir(), 'liku-automation'),
|
|
28
|
+
|
|
29
|
+
// Logging
|
|
30
|
+
DEBUG: process.env.LIKU_DEBUG === 'true',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Ensure temp directory exists
|
|
34
|
+
if (!fs.existsSync(CONFIG.TEMP_DIR)) {
|
|
35
|
+
fs.mkdirSync(CONFIG.TEMP_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// CONTROL TYPES
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Windows UI Automation control type constants
|
|
44
|
+
*/
|
|
45
|
+
const CONTROL_TYPES = {
|
|
46
|
+
BUTTON: 'Button',
|
|
47
|
+
CHECKBOX: 'CheckBox',
|
|
48
|
+
COMBOBOX: 'ComboBox',
|
|
49
|
+
EDIT: 'Edit',
|
|
50
|
+
HYPERLINK: 'Hyperlink',
|
|
51
|
+
IMAGE: 'Image',
|
|
52
|
+
LIST: 'List',
|
|
53
|
+
LISTITEM: 'ListItem',
|
|
54
|
+
MENU: 'Menu',
|
|
55
|
+
MENUITEM: 'MenuItem',
|
|
56
|
+
PANE: 'Pane',
|
|
57
|
+
PROGRESSBAR: 'ProgressBar',
|
|
58
|
+
RADIOBUTTON: 'RadioButton',
|
|
59
|
+
SCROLLBAR: 'ScrollBar',
|
|
60
|
+
SLIDER: 'Slider',
|
|
61
|
+
SPINNER: 'Spinner',
|
|
62
|
+
STATUSBAR: 'StatusBar',
|
|
63
|
+
TAB: 'Tab',
|
|
64
|
+
TABITEM: 'TabItem',
|
|
65
|
+
TEXT: 'Text',
|
|
66
|
+
TOOLBAR: 'Toolbar',
|
|
67
|
+
TOOLTIP: 'ToolTip',
|
|
68
|
+
TREE: 'Tree',
|
|
69
|
+
TREEITEM: 'TreeItem',
|
|
70
|
+
WINDOW: 'Window',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
CONFIG,
|
|
75
|
+
CONTROL_TYPES,
|
|
76
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Common utility functions for UI automation.
|
|
5
|
+
* @module ui-automation/core/helpers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { CONFIG } = require('../config');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sleep for specified milliseconds
|
|
12
|
+
* @param {number} ms - Milliseconds to sleep
|
|
13
|
+
* @returns {Promise<void>}
|
|
14
|
+
*/
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Log debug messages when DEBUG mode is enabled
|
|
21
|
+
* @param {...any} args - Arguments to log
|
|
22
|
+
*/
|
|
23
|
+
function debug(...args) {
|
|
24
|
+
if (CONFIG.DEBUG) {
|
|
25
|
+
console.log('[UI-AUTO DEBUG]', ...args);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Log automation actions
|
|
31
|
+
* @param {...any} args - Arguments to log
|
|
32
|
+
*/
|
|
33
|
+
function log(...args) {
|
|
34
|
+
console.log('[UI-AUTO]', ...args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
sleep,
|
|
39
|
+
debug,
|
|
40
|
+
log,
|
|
41
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core utilities for UI automation
|
|
3
|
+
* @module ui-automation/core
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { executePowerShellScript, executePowerShell } = require('./powershell');
|
|
7
|
+
const { sleep, debug, log } = require('./helpers');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
executePowerShellScript,
|
|
11
|
+
executePowerShell,
|
|
12
|
+
sleep,
|
|
13
|
+
debug,
|
|
14
|
+
log,
|
|
15
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell Execution Layer
|
|
3
|
+
*
|
|
4
|
+
* Provides reliable PowerShell script execution for UI automation.
|
|
5
|
+
* @module ui-automation/core/powershell
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { exec } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { CONFIG } = require('../config');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute a PowerShell script from a temp file
|
|
15
|
+
* More reliable than inline commands for complex scripts
|
|
16
|
+
*
|
|
17
|
+
* @param {string} script - PowerShell script content
|
|
18
|
+
* @param {number} [timeout] - Execution timeout in ms
|
|
19
|
+
* @returns {Promise<{stdout: string, stderr: string, error?: string}>}
|
|
20
|
+
*/
|
|
21
|
+
async function executePowerShellScript(script, timeout = CONFIG.DEFAULT_TIMEOUT) {
|
|
22
|
+
const scriptPath = path.join(
|
|
23
|
+
CONFIG.TEMP_DIR,
|
|
24
|
+
`script_${Date.now()}_${Math.random().toString(36).slice(2)}.ps1`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
fs.writeFileSync(scriptPath, script, 'utf8');
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
exec(
|
|
32
|
+
`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`,
|
|
33
|
+
{
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
maxBuffer: CONFIG.PS_MAX_BUFFER,
|
|
36
|
+
timeout: timeout,
|
|
37
|
+
},
|
|
38
|
+
(error, stdout, stderr) => {
|
|
39
|
+
// Clean up temp file
|
|
40
|
+
try { fs.unlinkSync(scriptPath); } catch {}
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
resolve({ stdout: stdout || '', stderr: stderr || '', error: error.message });
|
|
44
|
+
} else {
|
|
45
|
+
resolve({ stdout: stdout || '', stderr: stderr || '' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
try { fs.unlinkSync(scriptPath); } catch {}
|
|
52
|
+
return { stdout: '', stderr: '', error: err.message };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute a simple PowerShell command inline
|
|
58
|
+
*
|
|
59
|
+
* @param {string} command - PowerShell command
|
|
60
|
+
* @returns {Promise<string>} Command output
|
|
61
|
+
*/
|
|
62
|
+
async function executePowerShell(command) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const psCommand = command.replace(/"/g, '`"');
|
|
65
|
+
|
|
66
|
+
exec(`powershell -NoProfile -Command "${psCommand}"`, {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
maxBuffer: CONFIG.PS_MAX_BUFFER,
|
|
69
|
+
}, (error, stdout, stderr) => {
|
|
70
|
+
if (error) {
|
|
71
|
+
reject(new Error(stderr || error.message));
|
|
72
|
+
} else {
|
|
73
|
+
resolve(stdout.trim());
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
executePowerShellScript,
|
|
81
|
+
executePowerShell,
|
|
82
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element Discovery
|
|
3
|
+
*
|
|
4
|
+
* Find UI elements using Windows UI Automation.
|
|
5
|
+
* @module ui-automation/elements/finder
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { CONFIG } = require('../config');
|
|
9
|
+
const { executePowerShellScript } = require('../core/powershell');
|
|
10
|
+
const { debug, log } = require('../core/helpers');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ElementSearchOptions
|
|
14
|
+
* @property {string} [text] - Text/name to search for (partial match)
|
|
15
|
+
* @property {string} [exactText] - Exact text match
|
|
16
|
+
* @property {string} [automationId] - UI Automation AutomationId
|
|
17
|
+
* @property {string} [className] - Element class name
|
|
18
|
+
* @property {string} [controlType] - Control type (Button, Edit, ComboBox, etc.)
|
|
19
|
+
* @property {Object} [bounds] - Bounding constraints {minX, maxX, minY, maxY}
|
|
20
|
+
* @property {boolean} [isEnabled] - Filter by enabled state
|
|
21
|
+
* @property {string} [windowTitle] - Limit search to specific window
|
|
22
|
+
* @property {number} [index] - Select Nth matching element (0-based)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} UIElement
|
|
27
|
+
* @property {string} Name - Element accessible name
|
|
28
|
+
* @property {string} ControlType - UI Automation control type
|
|
29
|
+
* @property {string} AutomationId - Unique automation identifier
|
|
30
|
+
* @property {string} ClassName - Win32 class name
|
|
31
|
+
* @property {boolean} IsEnabled - Whether element accepts input
|
|
32
|
+
* @property {Object} Bounds - Bounding rectangle {X, Y, Width, Height, CenterX, CenterY}
|
|
33
|
+
* @property {string[]} Patterns - Supported UI Automation patterns
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find UI elements matching search criteria
|
|
38
|
+
* Uses Windows UI Automation for semantic element discovery
|
|
39
|
+
*
|
|
40
|
+
* @param {ElementSearchOptions} options - Search criteria
|
|
41
|
+
* @returns {Promise<{success: boolean, elements: UIElement[], count: number, error?: string}>}
|
|
42
|
+
*/
|
|
43
|
+
async function findElements(options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
text = '',
|
|
46
|
+
exactText = '',
|
|
47
|
+
automationId = '',
|
|
48
|
+
className = '',
|
|
49
|
+
controlType = '',
|
|
50
|
+
bounds = {},
|
|
51
|
+
isEnabled,
|
|
52
|
+
windowTitle = '',
|
|
53
|
+
index,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
const searchText = exactText || text;
|
|
57
|
+
const isExactMatch = !!exactText;
|
|
58
|
+
|
|
59
|
+
const psScript = `
|
|
60
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
61
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
62
|
+
|
|
63
|
+
function Find-UIElements {
|
|
64
|
+
param(
|
|
65
|
+
[string]$SearchText = "",
|
|
66
|
+
[bool]$ExactMatch = $false,
|
|
67
|
+
[string]$AutomationId = "",
|
|
68
|
+
[string]$ClassName = "",
|
|
69
|
+
[string]$ControlType = "",
|
|
70
|
+
[string]$WindowTitle = "",
|
|
71
|
+
[int]$MinX = [int]::MinValue,
|
|
72
|
+
[int]$MaxX = [int]::MaxValue,
|
|
73
|
+
[int]$MinY = [int]::MinValue,
|
|
74
|
+
[int]$MaxY = [int]::MaxValue,
|
|
75
|
+
[bool]$RequireEnabled = $false
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Get root element or specific window
|
|
79
|
+
$root = $null
|
|
80
|
+
if ($WindowTitle -ne "") {
|
|
81
|
+
$condition = [System.Windows.Automation.PropertyCondition]::new(
|
|
82
|
+
[System.Windows.Automation.AutomationElement]::NameProperty,
|
|
83
|
+
$WindowTitle,
|
|
84
|
+
[System.Windows.Automation.PropertyConditionFlags]::IgnoreCase
|
|
85
|
+
)
|
|
86
|
+
$windows = [System.Windows.Automation.AutomationElement]::RootElement.FindAll(
|
|
87
|
+
[System.Windows.Automation.TreeScope]::Children,
|
|
88
|
+
$condition
|
|
89
|
+
)
|
|
90
|
+
if ($windows.Count -gt 0) {
|
|
91
|
+
$root = $windows[0]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ($root -eq $null) {
|
|
96
|
+
$root = [System.Windows.Automation.AutomationElement]::RootElement
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Always search all elements, filter by ControlType in the loop
|
|
100
|
+
$searchCondition = [System.Windows.Automation.Condition]::TrueCondition
|
|
101
|
+
|
|
102
|
+
$elements = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
|
|
103
|
+
|
|
104
|
+
$results = @()
|
|
105
|
+
foreach ($el in $elements) {
|
|
106
|
+
try {
|
|
107
|
+
$name = $el.Current.Name
|
|
108
|
+
$ctrlType = $el.Current.ControlType.ProgrammaticName
|
|
109
|
+
$autoId = $el.Current.AutomationId
|
|
110
|
+
$cls = $el.Current.ClassName
|
|
111
|
+
$enabled = $el.Current.IsEnabled
|
|
112
|
+
$rect = $el.Current.BoundingRectangle
|
|
113
|
+
|
|
114
|
+
# Skip invisible elements
|
|
115
|
+
if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
|
|
116
|
+
if ([double]::IsInfinity($rect.X) -or [double]::IsInfinity($rect.Y)) { continue }
|
|
117
|
+
|
|
118
|
+
# Apply filters
|
|
119
|
+
if ($SearchText -ne "") {
|
|
120
|
+
$textMatch = $false
|
|
121
|
+
if ($ExactMatch) {
|
|
122
|
+
$textMatch = ($name -eq $SearchText)
|
|
123
|
+
} else {
|
|
124
|
+
$textMatch = ($name -like "*$SearchText*")
|
|
125
|
+
}
|
|
126
|
+
if (-not $textMatch) { continue }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if ($AutomationId -ne "" -and $autoId -notlike "*$AutomationId*") { continue }
|
|
130
|
+
if ($ClassName -ne "" -and $cls -notlike "*$ClassName*") { continue }
|
|
131
|
+
if ($ControlType -ne "" -and $ctrlType -notlike "*$ControlType*") { continue }
|
|
132
|
+
if ($RequireEnabled -and -not $enabled) { continue }
|
|
133
|
+
|
|
134
|
+
# Bounds filter
|
|
135
|
+
$centerX = [int]($rect.X + $rect.Width / 2)
|
|
136
|
+
$centerY = [int]($rect.Y + $rect.Height / 2)
|
|
137
|
+
if ($centerX -lt $MinX -or $centerX -gt $MaxX) { continue }
|
|
138
|
+
if ($centerY -lt $MinY -or $centerY -gt $MaxY) { continue }
|
|
139
|
+
|
|
140
|
+
# Get supported patterns
|
|
141
|
+
$patterns = @()
|
|
142
|
+
try {
|
|
143
|
+
$supportedPatterns = $el.GetSupportedPatterns()
|
|
144
|
+
foreach ($p in $supportedPatterns) {
|
|
145
|
+
$patterns += $p.ProgrammaticName
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
|
|
149
|
+
$results += @{
|
|
150
|
+
Name = $name
|
|
151
|
+
ControlType = $ctrlType.Replace("ControlType.", "")
|
|
152
|
+
AutomationId = $autoId
|
|
153
|
+
ClassName = $cls
|
|
154
|
+
IsEnabled = $enabled
|
|
155
|
+
Bounds = @{
|
|
156
|
+
X = [int]$rect.X
|
|
157
|
+
Y = [int]$rect.Y
|
|
158
|
+
Width = [int]$rect.Width
|
|
159
|
+
Height = [int]$rect.Height
|
|
160
|
+
CenterX = $centerX
|
|
161
|
+
CenterY = $centerY
|
|
162
|
+
}
|
|
163
|
+
Patterns = $patterns
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return $results
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$results = Find-UIElements \`
|
|
172
|
+
-SearchText "${searchText.replace(/"/g, '`"')}" \`
|
|
173
|
+
-ExactMatch $${isExactMatch} \`
|
|
174
|
+
-AutomationId "${automationId}" \`
|
|
175
|
+
-ClassName "${className}" \`
|
|
176
|
+
-ControlType "${controlType}" \`
|
|
177
|
+
-WindowTitle "${windowTitle.replace(/"/g, '`"')}" \`
|
|
178
|
+
${bounds.minX !== undefined ? `-MinX ${bounds.minX}` : ''} \`
|
|
179
|
+
${bounds.maxX !== undefined ? `-MaxX ${bounds.maxX}` : ''} \`
|
|
180
|
+
${bounds.minY !== undefined ? `-MinY ${bounds.minY}` : ''} \`
|
|
181
|
+
${bounds.maxY !== undefined ? `-MaxY ${bounds.maxY}` : ''} \`
|
|
182
|
+
-RequireEnabled $${isEnabled === true}
|
|
183
|
+
|
|
184
|
+
$results | ConvertTo-Json -Depth 5 -Compress
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
const result = await executePowerShellScript(psScript, 30000);
|
|
188
|
+
|
|
189
|
+
debug('PowerShell stdout:', result.stdout?.substring(0, 500));
|
|
190
|
+
debug('PowerShell stderr:', result.stderr);
|
|
191
|
+
debug('PowerShell error:', result.error);
|
|
192
|
+
|
|
193
|
+
if (result.error) {
|
|
194
|
+
return { success: false, elements: [], count: 0, error: result.error };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Handle empty results
|
|
199
|
+
const output = (result.stdout || '').trim();
|
|
200
|
+
if (!output) {
|
|
201
|
+
return { success: true, elements: [], count: 0, element: null };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let rawElements = JSON.parse(output);
|
|
205
|
+
if (!Array.isArray(rawElements)) {
|
|
206
|
+
rawElements = rawElements ? [rawElements] : [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Normalize element structure to camelCase
|
|
210
|
+
let elements = rawElements.map(e => ({
|
|
211
|
+
name: e.Name,
|
|
212
|
+
controlType: e.ControlType,
|
|
213
|
+
automationId: e.AutomationId,
|
|
214
|
+
className: e.ClassName,
|
|
215
|
+
isEnabled: e.IsEnabled,
|
|
216
|
+
patterns: e.Patterns,
|
|
217
|
+
bounds: e.Bounds ? {
|
|
218
|
+
x: e.Bounds.X,
|
|
219
|
+
y: e.Bounds.Y,
|
|
220
|
+
width: e.Bounds.Width,
|
|
221
|
+
height: e.Bounds.Height,
|
|
222
|
+
centerX: e.Bounds.CenterX,
|
|
223
|
+
centerY: e.Bounds.CenterY,
|
|
224
|
+
} : null,
|
|
225
|
+
// Keep original PascalCase for backward compatibility
|
|
226
|
+
Name: e.Name,
|
|
227
|
+
ControlType: e.ControlType,
|
|
228
|
+
AutomationId: e.AutomationId,
|
|
229
|
+
ClassName: e.ClassName,
|
|
230
|
+
IsEnabled: e.IsEnabled,
|
|
231
|
+
Patterns: e.Patterns,
|
|
232
|
+
Bounds: e.Bounds,
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// Apply index filter if specified
|
|
236
|
+
if (typeof index === 'number' && index >= 0 && index < elements.length) {
|
|
237
|
+
elements = [elements[index]];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
log(`Found ${elements.length} element(s) matching criteria`);
|
|
241
|
+
debug('Search options:', options);
|
|
242
|
+
debug('Results:', elements.map(e => `${e.name} (${e.controlType})`));
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
elements,
|
|
247
|
+
count: elements.length,
|
|
248
|
+
element: elements[0] || null,
|
|
249
|
+
};
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { success: false, elements: [], count: 0, error: `Parse error: ${e.message}`, raw: result.stdout };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Find a single element matching criteria
|
|
257
|
+
* Convenience wrapper around findElements
|
|
258
|
+
*
|
|
259
|
+
* @param {ElementSearchOptions} options - Search criteria
|
|
260
|
+
* @returns {Promise<{success: boolean, element: UIElement|null, error?: string}>}
|
|
261
|
+
*/
|
|
262
|
+
async function findElement(options = {}) {
|
|
263
|
+
const result = await findElements({ ...options, index: 0 });
|
|
264
|
+
return {
|
|
265
|
+
success: result.success && result.element !== null,
|
|
266
|
+
element: result.element,
|
|
267
|
+
error: result.element ? undefined : result.error || 'Element not found',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
findElements,
|
|
273
|
+
findElement,
|
|
274
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element Discovery and Wait Utilities
|
|
3
|
+
* @module ui-automation/elements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { findElements, findElement } = require('./finder');
|
|
7
|
+
const { waitForElement, waitForElementGone } = require('./wait');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
findElements,
|
|
11
|
+
findElement,
|
|
12
|
+
waitForElement,
|
|
13
|
+
waitForElementGone,
|
|
14
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element Wait Utilities
|
|
3
|
+
*
|
|
4
|
+
* Wait for elements to appear or disappear.
|
|
5
|
+
* @module ui-automation/elements/wait
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { CONFIG } = require('../config');
|
|
9
|
+
const { sleep } = require('../core/helpers');
|
|
10
|
+
const { findElement } = require('./finder');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wait for an element to appear
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options - Search criteria (same as findElement)
|
|
16
|
+
* @param {number} [timeout=10000] - Maximum wait time in ms
|
|
17
|
+
* @returns {Promise<{success: boolean, element: Object|null, elapsed: number, error?: string}>}
|
|
18
|
+
*/
|
|
19
|
+
async function waitForElement(options = {}, timeout = CONFIG.DEFAULT_TIMEOUT) {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
|
|
22
|
+
while (Date.now() - startTime < timeout) {
|
|
23
|
+
const result = await findElement(options);
|
|
24
|
+
if (result.success && result.element) {
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
element: result.element,
|
|
28
|
+
elapsed: Date.now() - startTime,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
await sleep(CONFIG.ELEMENT_WAIT_INTERVAL);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
element: null,
|
|
37
|
+
elapsed: Date.now() - startTime,
|
|
38
|
+
error: `Element not found within ${timeout}ms`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wait for an element to disappear
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} options - Search criteria (same as findElement)
|
|
46
|
+
* @param {number} [timeout=10000] - Maximum wait time in ms
|
|
47
|
+
* @returns {Promise<{success: boolean, elapsed: number}>}
|
|
48
|
+
*/
|
|
49
|
+
async function waitForElementGone(options = {}, timeout = CONFIG.DEFAULT_TIMEOUT) {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
|
|
52
|
+
while (Date.now() - startTime < timeout) {
|
|
53
|
+
const result = await findElement(options);
|
|
54
|
+
if (!result.success || !result.element) {
|
|
55
|
+
return { success: true, elapsed: Date.now() - startTime };
|
|
56
|
+
}
|
|
57
|
+
await sleep(CONFIG.ELEMENT_WAIT_INTERVAL);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { success: false, elapsed: Date.now() - startTime };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
waitForElement,
|
|
65
|
+
waitForElementGone,
|
|
66
|
+
};
|