@gurugokul/doomscroller 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +21 -0
  3. package/README.md +323 -2
  4. package/dist/adapters/claude-code-hooks.js +27 -0
  5. package/dist/adapters/claude-code-hooks.js.map +1 -0
  6. package/dist/adapters/cli-wrapper.js +230 -0
  7. package/dist/adapters/cli-wrapper.js.map +1 -0
  8. package/dist/adapters/command-code-hooks.js +32 -0
  9. package/dist/adapters/command-code-hooks.js.map +1 -0
  10. package/dist/adapters/file-activity.js +78 -0
  11. package/dist/adapters/file-activity.js.map +1 -0
  12. package/dist/adapters/hook-settings.js +164 -0
  13. package/dist/adapters/hook-settings.js.map +1 -0
  14. package/dist/adapters/hook.js +17 -0
  15. package/dist/adapters/hook.js.map +1 -0
  16. package/dist/adapters/hotkey.js +50 -0
  17. package/dist/adapters/hotkey.js.map +1 -0
  18. package/dist/adapters/ide-activity.js +245 -0
  19. package/dist/adapters/ide-activity.js.map +1 -0
  20. package/dist/adapters/ports.js +3 -0
  21. package/dist/adapters/ports.js.map +1 -0
  22. package/dist/adapters/wrapper-tracker.js +77 -0
  23. package/dist/adapters/wrapper-tracker.js.map +1 -0
  24. package/dist/cli.js +343 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/core/config.js +192 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/daemon.js +156 -0
  29. package/dist/core/daemon.js.map +1 -0
  30. package/dist/core/ipc.js +215 -0
  31. package/dist/core/ipc.js.map +1 -0
  32. package/dist/core/logger.js +53 -0
  33. package/dist/core/logger.js.map +1 -0
  34. package/dist/core/sound.js +38 -0
  35. package/dist/core/sound.js.map +1 -0
  36. package/dist/core/state-machine.js +272 -0
  37. package/dist/core/state-machine.js.map +1 -0
  38. package/dist/core/stats.js +109 -0
  39. package/dist/core/stats.js.map +1 -0
  40. package/dist/core/targets.js +27 -0
  41. package/dist/core/targets.js.map +1 -0
  42. package/dist/core/types.js +6 -0
  43. package/dist/core/types.js.map +1 -0
  44. package/dist/index.js +15 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/util/errors.js +16 -0
  47. package/dist/util/errors.js.map +1 -0
  48. package/dist/util/paths.js +77 -0
  49. package/dist/util/paths.js.map +1 -0
  50. package/dist/windows/cpu-probe.js +108 -0
  51. package/dist/windows/cpu-probe.js.map +1 -0
  52. package/dist/windows/key-listener.js +198 -0
  53. package/dist/windows/key-listener.js.map +1 -0
  54. package/dist/windows/win32.js +158 -0
  55. package/dist/windows/win32.js.map +1 -0
  56. package/dist/windows/window-controller.js +3 -0
  57. package/dist/windows/window-controller.js.map +1 -0
  58. package/package.json +57 -4
  59. package/scripts/cpu-sampler.ps1 +83 -0
  60. package/scripts/key-watcher.ps1 +132 -0
  61. package/scripts/win32-window.ps1 +344 -0
@@ -0,0 +1,132 @@
1
+ param(
2
+ [int]$ParentPid = 0,
3
+ [string]$HotkeyKey = 'D',
4
+ [int]$IntervalMs = 15
5
+ )
6
+
7
+ $ErrorActionPreference = 'Stop'
8
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
9
+ $OutputEncoding = New-Object System.Text.UTF8Encoding($false)
10
+
11
+ Add-Type -TypeDefinition @'
12
+ using System;
13
+ using System.Text;
14
+ using System.Runtime.InteropServices;
15
+
16
+ public class DSDoomKeys
17
+ {
18
+ [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vKey);
19
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
20
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
21
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLength(IntPtr hWnd);
22
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
23
+
24
+ public static string Title(IntPtr h)
25
+ {
26
+ int length = GetWindowTextLength(h);
27
+ if (length <= 0) return string.Empty;
28
+ var builder = new StringBuilder(length + 1);
29
+ GetWindowText(h, builder, builder.Capacity);
30
+ return builder.ToString();
31
+ }
32
+
33
+ public static uint ProcessId(IntPtr h)
34
+ {
35
+ uint processId;
36
+ GetWindowThreadProcessId(h, out processId);
37
+ return processId;
38
+ }
39
+ }
40
+ '@
41
+
42
+ $specialKeys = @{
43
+ 'ENTER' = 0x0D
44
+ 'SPACE' = 0x20
45
+ 'TAB' = 0x09
46
+ 'ESC' = 0x1B
47
+ 'BACKSPACE' = 0x08
48
+ }
49
+ for ($index = 0; $index -lt 26; $index++) {
50
+ $specialKeys[[string][char](65 + $index)] = 65 + $index
51
+ }
52
+ for ($index = 0; $index -lt 10; $index++) {
53
+ $specialKeys[[string][char](48 + $index)] = 48 + $index
54
+ }
55
+ for ($index = 1; $index -le 12; $index++) {
56
+ $specialKeys["F$index"] = 111 + $index
57
+ }
58
+
59
+ $requested = ([string]$HotkeyKey).Trim().ToUpperInvariant()
60
+ if ($requested.StartsWith('VK_')) { $requested = $requested.Substring(3) }
61
+ if (-not $specialKeys.ContainsKey($requested)) { $requested = 'D' }
62
+
63
+ $watch = @{ 'ENTER' = 0x0D }
64
+ if (-not $watch.ContainsKey($requested)) { $watch[$requested] = $specialKeys[$requested] }
65
+
66
+ function Write-Result([object]$Value) {
67
+ $json = ConvertTo-Json -InputObject $Value -Compress -Depth 4
68
+ [Console]::Out.WriteLine($json)
69
+ [Console]::Out.Flush()
70
+ }
71
+
72
+ Write-Result @{ type = 'ready'; keys = @($watch.Keys) }
73
+
74
+ $down = @{}
75
+ $iteration = 0
76
+
77
+ while ($true) {
78
+ foreach ($keyName in @($watch.Keys)) {
79
+ $virtualKey = $watch[$keyName]
80
+ $isDown = (([DSDoomKeys]::GetAsyncKeyState($virtualKey)) -band 0x8000) -ne 0
81
+
82
+ if ($isDown -and -not $down.ContainsKey($keyName)) {
83
+ $down[$keyName] = $true
84
+
85
+ $ctrl = (([DSDoomKeys]::GetAsyncKeyState(0x11)) -band 0x8000) -ne 0
86
+ $alt = (([DSDoomKeys]::GetAsyncKeyState(0x12)) -band 0x8000) -ne 0
87
+ $shift = (([DSDoomKeys]::GetAsyncKeyState(0x10)) -band 0x8000) -ne 0
88
+
89
+ $windowHandle = [DSDoomKeys]::GetForegroundWindow()
90
+ $windowTitle = ''
91
+ $processNameValue = ''
92
+ $processIdValue = 0
93
+
94
+ if ($windowHandle -ne [IntPtr]::Zero) {
95
+ $windowTitle = [DSDoomKeys]::Title($windowHandle)
96
+ $processIdValue = [int][DSDoomKeys]::ProcessId($windowHandle)
97
+ if ($processIdValue -ne 0) {
98
+ try {
99
+ $processNameValue = (Get-Process -Id $processIdValue -ErrorAction Stop).ProcessName
100
+ } catch {
101
+ }
102
+ }
103
+ }
104
+
105
+ Write-Result @{
106
+ type = 'key'
107
+ key = $keyName
108
+ ctrl = $ctrl
109
+ alt = $alt
110
+ shift = $shift
111
+ hwnd = $windowHandle.ToInt64().ToString([System.Globalization.CultureInfo]::InvariantCulture)
112
+ title = $windowTitle
113
+ process = $processNameValue
114
+ pid = $processIdValue
115
+ at = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
116
+ }
117
+ } elseif (-not $isDown -and $down.ContainsKey($keyName)) {
118
+ $down.Remove($keyName)
119
+ }
120
+ }
121
+
122
+ $iteration++
123
+ if ($ParentPid -gt 0 -and ($iteration % 100 -eq 0)) {
124
+ try {
125
+ $null = Get-Process -Id $ParentPid -ErrorAction Stop
126
+ } catch {
127
+ break
128
+ }
129
+ }
130
+
131
+ [System.Threading.Thread]::Sleep($IntervalMs)
132
+ }
@@ -0,0 +1,344 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [ValidateSet('get-foreground', 'minimize', 'restore', 'focus', 'find', 'open-url')]
4
+ [string]$Action,
5
+ [string]$Handle = '',
6
+ [string]$Title = '',
7
+ [string]$ProcessName = '',
8
+ [string]$Url = '',
9
+ [ValidateSet('auto', 'edge', 'chrome', 'brave', 'firefox', 'default')]
10
+ [string]$Browser = 'auto'
11
+ )
12
+
13
+ $ErrorActionPreference = 'Stop'
14
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
15
+ $OutputEncoding = New-Object System.Text.UTF8Encoding($false)
16
+
17
+ Add-Type -TypeDefinition @'
18
+ using System;
19
+ using System.Text;
20
+ using System.Collections.Generic;
21
+ using System.Runtime.InteropServices;
22
+
23
+ public class DSDoomWin32
24
+ {
25
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
26
+
27
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
28
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
29
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
30
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
31
+ [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
32
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
33
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
34
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
35
+ [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
36
+ [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
37
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
38
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);
39
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLength(IntPtr hWnd);
40
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
41
+
42
+ public static List<IntPtr> Windows()
43
+ {
44
+ var list = new List<IntPtr>();
45
+ EnumWindows(delegate(IntPtr h, IntPtr l) { list.Add(h); return true; }, IntPtr.Zero);
46
+ return list;
47
+ }
48
+
49
+ public static string Title(IntPtr h)
50
+ {
51
+ int length = GetWindowTextLength(h);
52
+ if (length <= 0) return string.Empty;
53
+ var builder = new StringBuilder(length + 1);
54
+ GetWindowText(h, builder, builder.Capacity);
55
+ return builder.ToString();
56
+ }
57
+
58
+ public static uint ProcessId(IntPtr h)
59
+ {
60
+ uint processId;
61
+ GetWindowThreadProcessId(h, out processId);
62
+ return processId;
63
+ }
64
+
65
+ public static bool Focus(IntPtr h, bool altTap)
66
+ {
67
+ if (h == IntPtr.Zero || !IsWindow(h)) return false;
68
+ if (IsIconic(h)) ShowWindow(h, 9);
69
+
70
+ IntPtr foreground = GetForegroundWindow();
71
+ uint foregroundPid;
72
+ uint foregroundThread = GetWindowThreadProcessId(foreground, out foregroundPid);
73
+ uint currentThread = GetCurrentThreadId();
74
+ bool attached = false;
75
+ if (foregroundThread != 0 && foregroundThread != currentThread)
76
+ {
77
+ attached = AttachThreadInput(currentThread, foregroundThread, true);
78
+ }
79
+
80
+ if (altTap)
81
+ {
82
+ // Windows refuses SetForegroundWindow unless the calling process owns
83
+ // the foreground lock. A synthetic Alt tap releases that lock.
84
+ keybd_event(0x12, 0, 0, UIntPtr.Zero);
85
+ System.Threading.Thread.Sleep(45);
86
+ keybd_event(0x12, 0, 2, UIntPtr.Zero);
87
+ }
88
+
89
+ BringWindowToTop(h);
90
+ bool focused = SetForegroundWindow(h);
91
+
92
+ if (attached)
93
+ {
94
+ AttachThreadInput(currentThread, foregroundThread, false);
95
+ }
96
+ return focused;
97
+ }
98
+ }
99
+ '@
100
+
101
+ function Write-Result([object]$Value) {
102
+ $json = ConvertTo-Json -InputObject $Value -Compress -Depth 6
103
+ [Console]::Out.WriteLine($json)
104
+ [Console]::Out.Flush()
105
+ }
106
+
107
+ function Get-WindowInfo([IntPtr]$WindowHandle) {
108
+ $processIdValue = [DSDoomWin32]::ProcessId($WindowHandle)
109
+ $processNameValue = ''
110
+ if ($processIdValue -ne 0) {
111
+ try {
112
+ $processNameValue = (Get-Process -Id ([int]$processIdValue) -ErrorAction Stop).ProcessName
113
+ } catch {
114
+ }
115
+ }
116
+ [PSCustomObject]@{
117
+ id = $WindowHandle.ToInt64().ToString([System.Globalization.CultureInfo]::InvariantCulture)
118
+ title = [DSDoomWin32]::Title($WindowHandle)
119
+ processName = $processNameValue
120
+ pid = [int]$processIdValue
121
+ }
122
+ }
123
+
124
+ function Test-UsableWindow([IntPtr]$WindowHandle) {
125
+ if ($WindowHandle -eq [IntPtr]::Zero) { return $false }
126
+ if (-not [DSDoomWin32]::IsWindowVisible($WindowHandle)) { return $false }
127
+ return $true
128
+ }
129
+
130
+ function Get-MatchingWindows {
131
+ $wantedProcess = $ProcessName.Trim().ToLowerInvariant()
132
+ if ($wantedProcess.EndsWith('.exe')) {
133
+ $wantedProcess = $wantedProcess.Substring(0, $wantedProcess.Length - 4)
134
+ }
135
+ $wantedTitle = $Title.Trim().ToLowerInvariant()
136
+
137
+ $result = New-Object System.Collections.ArrayList
138
+ if ($wantedProcess -eq '' -and $wantedTitle -eq '') {
139
+ return @($result)
140
+ }
141
+
142
+ foreach ($windowHandle in [DSDoomWin32]::Windows()) {
143
+ if (-not (Test-UsableWindow $windowHandle)) { continue }
144
+ $info = Get-WindowInfo $windowHandle
145
+ if ($info.pid -eq 0) { continue }
146
+ if ([string]::IsNullOrWhiteSpace($info.title)) { continue }
147
+ if ($wantedProcess -ne '' -and $info.processName.ToLowerInvariant() -ne $wantedProcess) { continue }
148
+ if ($wantedTitle -ne '' -and -not $info.title.ToLowerInvariant().Contains($wantedTitle)) { continue }
149
+ [void]$result.Add($info)
150
+ }
151
+ return @($result)
152
+ }
153
+
154
+ function Resolve-BrowserExe([string]$Choice) {
155
+ $programFiles = [Environment]::GetFolderPath('ProgramFiles')
156
+ $programFilesX86 = [Environment]::GetFolderPath('ProgramFilesX86')
157
+ $localAppData = [Environment]::GetFolderPath('LocalApplicationData')
158
+ if ([string]::IsNullOrWhiteSpace($programFilesX86)) { $programFilesX86 = ${env:ProgramFiles(x86)} }
159
+
160
+ $candidates = @()
161
+ switch ($Choice) {
162
+ 'edge' {
163
+ $candidates = @(
164
+ (Join-Path $programFilesX86 'Microsoft\Edge\Application\msedge.exe'),
165
+ (Join-Path $programFiles 'Microsoft\Edge\Application\msedge.exe')
166
+ )
167
+ }
168
+ 'chrome' {
169
+ $candidates = @(
170
+ (Join-Path $programFiles 'Google\Chrome\Application\chrome.exe'),
171
+ (Join-Path $programFilesX86 'Google\Chrome\Application\chrome.exe'),
172
+ (Join-Path $localAppData 'Google\Chrome\Application\chrome.exe')
173
+ )
174
+ }
175
+ 'brave' {
176
+ $candidates = @(
177
+ (Join-Path $programFiles 'BraveSoftware\Brave-Browser\Application\brave.exe'),
178
+ (Join-Path $programFilesX86 'BraveSoftware\Brave-Browser\Application\brave.exe'),
179
+ (Join-Path $localAppData 'BraveSoftware\Brave-Browser\Application\brave.exe')
180
+ )
181
+ }
182
+ 'firefox' {
183
+ $candidates = @(
184
+ (Join-Path $programFiles 'Mozilla Firefox\firefox.exe'),
185
+ (Join-Path $programFilesX86 'Mozilla Firefox\firefox.exe')
186
+ )
187
+ }
188
+ }
189
+
190
+ foreach ($candidate in $candidates) {
191
+ if ($candidate -and (Test-Path -LiteralPath $candidate)) { return $candidate }
192
+ }
193
+ return $null
194
+ }
195
+
196
+ function Get-DefaultBrowserKey {
197
+ try {
198
+ $choice = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice' -ErrorAction Stop
199
+ $progId = [string]$choice.ProgId
200
+ if ($progId -match 'ChromeHTML') { return 'chrome' }
201
+ if ($progId -match 'MSEdge') { return 'edge' }
202
+ if ($progId -match 'Brave') { return 'brave' }
203
+ if ($progId -match 'Firefox') { return 'firefox' }
204
+ } catch {
205
+ }
206
+ return $null
207
+ }
208
+
209
+ function Get-BrowserWindowIds([string[]]$ProcessNames) {
210
+ $ids = @{}
211
+ foreach ($windowHandle in [DSDoomWin32]::Windows()) {
212
+ if (-not (Test-UsableWindow $windowHandle)) { continue }
213
+ $info = Get-WindowInfo $windowHandle
214
+ if ($info.pid -eq 0) { continue }
215
+ if ($ProcessNames -contains $info.processName.ToLowerInvariant()) {
216
+ $ids[$info.id] = $true
217
+ }
218
+ }
219
+ return $ids
220
+ }
221
+
222
+ function Invoke-OpenUrl {
223
+ $knownBrowsers = @('msedge', 'chrome', 'brave', 'firefox', 'opera', 'vivaldi', 'arc', 'iexplore')
224
+ $resolvedChoice = $Browser
225
+ if ($resolvedChoice -eq 'auto') {
226
+ $resolvedChoice = Get-DefaultBrowserKey
227
+ if (-not $resolvedChoice) { $resolvedChoice = 'edge' }
228
+ }
229
+
230
+ $exe = $null
231
+ if ($resolvedChoice -ne 'default') {
232
+ $exe = Resolve-BrowserExe $resolvedChoice
233
+ if (-not $exe) {
234
+ $fallback = Resolve-BrowserExe 'edge'
235
+ if (-not $fallback) { $fallback = Resolve-BrowserExe 'chrome' }
236
+ $exe = $fallback
237
+ }
238
+ }
239
+
240
+ $expectedProcess = $null
241
+ if ($exe) {
242
+ $expectedProcess = [System.IO.Path]::GetFileNameWithoutExtension($exe).ToLowerInvariant()
243
+ if (-not $knownBrowsers.Contains($expectedProcess)) {
244
+ $knownBrowsers += $expectedProcess
245
+ }
246
+ }
247
+
248
+ $before = Get-BrowserWindowIds $knownBrowsers
249
+
250
+ if ($exe) {
251
+ if ($expectedProcess -eq 'firefox') {
252
+ Start-Process -FilePath $exe -ArgumentList @('-new-window', $Url) | Out-Null
253
+ } else {
254
+ Start-Process -FilePath $exe -ArgumentList @('--new-window', $Url) | Out-Null
255
+ }
256
+ } else {
257
+ Start-Process $Url | Out-Null
258
+ }
259
+
260
+ $deadline = (Get-Date).AddSeconds(8)
261
+ $found = $null
262
+ while ((Get-Date) -lt $deadline) {
263
+ Start-Sleep -Milliseconds 150
264
+ foreach ($windowHandle in [DSDoomWin32]::Windows()) {
265
+ if (-not (Test-UsableWindow $windowHandle)) { continue }
266
+ $info = Get-WindowInfo $windowHandle
267
+ if ($info.pid -eq 0) { continue }
268
+ if ($before.ContainsKey($info.id)) { continue }
269
+ $processNameValue = $info.processName.ToLowerInvariant()
270
+ if ($expectedProcess) {
271
+ if ($processNameValue -ne $expectedProcess) { continue }
272
+ } elseif (-not $knownBrowsers.Contains($processNameValue)) {
273
+ continue
274
+ }
275
+ if ([string]::IsNullOrWhiteSpace($info.title)) { continue }
276
+ $found = $info
277
+ break
278
+ }
279
+ if ($found) { break }
280
+ }
281
+
282
+ return $found
283
+ }
284
+
285
+ try {
286
+ switch ($Action) {
287
+ 'get-foreground' {
288
+ $windowHandle = [DSDoomWin32]::GetForegroundWindow()
289
+ if (-not (Test-UsableWindow $windowHandle)) {
290
+ Write-Result @{ ok = $false; error = 'no foreground window' }
291
+ } else {
292
+ Write-Result @{ ok = $true; window = (Get-WindowInfo $windowHandle) }
293
+ }
294
+ }
295
+ 'find' {
296
+ Write-Result @{ ok = $true; windows = @(Get-MatchingWindows) }
297
+ }
298
+ 'minimize' {
299
+ $windowHandle = [IntPtr]([Int64]$Handle)
300
+ if (-not [DSDoomWin32]::IsWindow($windowHandle)) {
301
+ Write-Result @{ ok = $false; error = 'invalid window handle' }
302
+ } else {
303
+ [void][DSDoomWin32]::ShowWindow($windowHandle, 6)
304
+ Write-Result @{ ok = $true }
305
+ }
306
+ }
307
+ 'restore' {
308
+ $windowHandle = [IntPtr]([Int64]$Handle)
309
+ if (-not [DSDoomWin32]::IsWindow($windowHandle)) {
310
+ Write-Result @{ ok = $false; error = 'invalid window handle' }
311
+ } else {
312
+ [void][DSDoomWin32]::ShowWindow($windowHandle, 9)
313
+ Write-Result @{ ok = $true }
314
+ }
315
+ }
316
+ 'focus' {
317
+ $windowHandle = [IntPtr]([Int64]$Handle)
318
+ if (-not [DSDoomWin32]::IsWindow($windowHandle)) {
319
+ Write-Result @{ ok = $false; error = 'invalid window handle' }
320
+ } else {
321
+ $focused = [DSDoomWin32]::Focus($windowHandle, $true)
322
+ if (-not $focused) {
323
+ Start-Sleep -Milliseconds 120
324
+ $focused = [DSDoomWin32]::Focus($windowHandle, $true)
325
+ }
326
+ Write-Result @{ ok = $true; focused = $focused }
327
+ }
328
+ }
329
+ 'open-url' {
330
+ $windowInfo = Invoke-OpenUrl
331
+ if ($windowInfo) {
332
+ Write-Result @{ ok = $true; window = $windowInfo }
333
+ } else {
334
+ Write-Result @{ ok = $false; error = 'browser window was not detected' }
335
+ }
336
+ }
337
+ default {
338
+ Write-Result @{ ok = $false; error = "unknown action: $Action" }
339
+ }
340
+ }
341
+ } catch {
342
+ Write-Result @{ ok = $false; error = $_.Exception.Message }
343
+ exit 0
344
+ }