openclacky 1.5.13 → 1.5.14
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +38 -97
- data/lib/clacky/agent.rb +281 -276
- data/lib/clacky/agent_config.rb +1 -1
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +6 -0
- data/lib/clacky/brand_config.rb +13 -2
- data/lib/clacky/cli.rb +27 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +91 -5
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +1 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +38 -3
- data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +8 -5
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +161 -14
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +79 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +2 -1
- data/lib/clacky/ui2/components/input_area.rb +22 -2
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/ui_controller.rb +44 -7
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +114 -66
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +596 -130
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/model-picker.js +2 -1
- data/lib/clacky/web/components/quote-select.js +235 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +392 -40
- data/lib/clacky/web/i18n.js +90 -5
- data/lib/clacky/web/index.html +15 -6
- data/lib/clacky/web/sessions.js +193 -38
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +20 -0
- data/lib/clacky.rb +2 -0
- metadata +11 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "json"
|
|
7
|
+
require_relative "environment_detector"
|
|
8
|
+
require_relative "encoding"
|
|
9
|
+
|
|
10
|
+
module Clacky
|
|
11
|
+
module Utils
|
|
12
|
+
# Detects installed Windows applications able to open a given file type.
|
|
13
|
+
#
|
|
14
|
+
# Windows file associations live in the registry (HKEY_CLASSES_ROOT / the
|
|
15
|
+
# per-user FileExts tree) rather than in app bundles, so this module shells
|
|
16
|
+
# out to PowerShell to read them. The result set mirrors what the Explorer
|
|
17
|
+
# "Open with" dialog offers: the user's default handler, the ProgIDs that
|
|
18
|
+
# declared support for the extension, and the user's recent "open with"
|
|
19
|
+
# choices. Desktop apps resolve to their .exe; packaged (UWP/Store) apps
|
|
20
|
+
# resolve to their AppUserModelID and are launched via shell:appsFolder.
|
|
21
|
+
# Non-WSL hosts always answer with an empty list.
|
|
22
|
+
module WindowsAppDetector
|
|
23
|
+
# `$Ext` is injected as a single-quoted literal after sanitisation, so it
|
|
24
|
+
# is safe to interpolate. Writes UTF-8 so Chinese app names survive the
|
|
25
|
+
# pipe (PowerShell 5.1 would otherwise emit the OEM codepage).
|
|
26
|
+
DETECT_SCRIPT = <<~'PWSH'
|
|
27
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
28
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
29
|
+
$Ext = '__CLACKY_EXT__'
|
|
30
|
+
|
|
31
|
+
function Parse-ExePath([string]$cmd) {
|
|
32
|
+
if ([string]::IsNullOrWhiteSpace($cmd)) { return $null }
|
|
33
|
+
$cmd = $cmd.Trim()
|
|
34
|
+
if ($cmd.Length -eq 0) { return $null }
|
|
35
|
+
$exe = $null
|
|
36
|
+
if ($cmd[0] -eq '"') {
|
|
37
|
+
$end = $cmd.IndexOf('"', 1)
|
|
38
|
+
if ($end -gt 1) { $exe = $cmd.Substring(1, $end - 1) }
|
|
39
|
+
} else {
|
|
40
|
+
$idx = $cmd.IndexOf(' ')
|
|
41
|
+
if ($idx -lt 0) { $idx = $cmd.IndexOf("`t") }
|
|
42
|
+
if ($idx -gt 0) { $exe = $cmd.Substring(0, $idx) } else { $exe = $cmd }
|
|
43
|
+
}
|
|
44
|
+
if ($exe) { $exe = [Environment]::ExpandEnvironmentVariables($exe) }
|
|
45
|
+
return $exe
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Get-DefaultValue([string]$keyPath) {
|
|
49
|
+
$p = Get-Item -Path $keyPath -ErrorAction SilentlyContinue
|
|
50
|
+
if ($p) { return $p.GetValue("") }
|
|
51
|
+
return $null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$nameMap = @{}
|
|
55
|
+
Get-StartApps -ErrorAction SilentlyContinue | ForEach-Object {
|
|
56
|
+
if ($_.AppID) { $nameMap[$_.AppID] = $_.Name }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$ext = $Ext.TrimStart('.').ToLower()
|
|
60
|
+
$script:apps = @()
|
|
61
|
+
$script:defaultApp = $null
|
|
62
|
+
$script:seen = @{}
|
|
63
|
+
|
|
64
|
+
function Add-App([string]$launch, [string]$name, [bool]$isDefault) {
|
|
65
|
+
if ([string]::IsNullOrWhiteSpace($launch)) { return }
|
|
66
|
+
# A bare .exe name (no directory) is a stale or PATH-only reference.
|
|
67
|
+
# Resolve it against PATH / App Paths so both launching and icon
|
|
68
|
+
# extraction work; skip the entry when it can't be resolved.
|
|
69
|
+
if ($launch -match '(?i)\.exe$' -and $launch -notmatch '[\\/]') {
|
|
70
|
+
$resolved = (Get-Command $launch -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
|
71
|
+
if (-not $resolved) {
|
|
72
|
+
$resolved = Get-DefaultValue "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$launch"
|
|
73
|
+
}
|
|
74
|
+
if ([string]::IsNullOrWhiteSpace($resolved) -or -not (Test-Path $resolved)) { return }
|
|
75
|
+
$launch = $resolved
|
|
76
|
+
}
|
|
77
|
+
$key = $launch.ToLower()
|
|
78
|
+
if ($script:seen.ContainsKey($key)) { return }
|
|
79
|
+
$script:seen[$key] = $true
|
|
80
|
+
if ([string]::IsNullOrWhiteSpace($name)) {
|
|
81
|
+
if ($script:nameMap.ContainsKey($launch)) {
|
|
82
|
+
$name = $script:nameMap[$launch]
|
|
83
|
+
} elseif ($launch -match '\.exe$') {
|
|
84
|
+
$name = [System.IO.Path]::GetFileNameWithoutExtension($launch)
|
|
85
|
+
} else {
|
|
86
|
+
$name = ($launch -split '!')[0]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
$entry = @{ name = $name; path = $launch }
|
|
90
|
+
$script:apps += $entry
|
|
91
|
+
if ($isDefault -and -not $script:defaultApp) { $script:defaultApp = $entry }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Add-ProgId([string]$progid, [bool]$isDefault) {
|
|
95
|
+
if ([string]::IsNullOrWhiteSpace($progid)) { return }
|
|
96
|
+
$cmdKey = "Registry::HKEY_CLASSES_ROOT\$progid\shell\open\command"
|
|
97
|
+
$cmdItem = Get-Item -Path $cmdKey -ErrorAction SilentlyContinue
|
|
98
|
+
$exe = if ($cmdItem) { Parse-ExePath ([string]$cmdItem.GetValue("")) } else { $null }
|
|
99
|
+
$name = Get-DefaultValue "Registry::HKEY_CLASSES_ROOT\$progid"
|
|
100
|
+
|
|
101
|
+
# Desktop app: a real exe outside the packaged-apps directory.
|
|
102
|
+
if ($exe -and $exe -notmatch '\\WindowsApps\\') {
|
|
103
|
+
Add-App $exe ([string]$name) $isDefault
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
# Packaged (UWP) app: launch via its AppUserModelID; the packaged
|
|
107
|
+
# exe under WindowsApps cannot be launched directly.
|
|
108
|
+
$appItem = Get-Item "Registry::HKEY_CLASSES_ROOT\$progid\Application" -ErrorAction SilentlyContinue
|
|
109
|
+
$aumid = if ($appItem) { [string]$appItem.GetValue("AppUserModelID") } else { $null }
|
|
110
|
+
if ($aumid) {
|
|
111
|
+
Add-App $aumid ([string]$name) $isDefault
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$ucItem = Get-Item "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.$ext\UserChoice" -ErrorAction SilentlyContinue
|
|
116
|
+
$userChoice = if ($ucItem) { $ucItem.GetValue("ProgId") } else { $null }
|
|
117
|
+
if ($userChoice) { Add-ProgId ([string]$userChoice) $true }
|
|
118
|
+
|
|
119
|
+
$defaultProg = Get-DefaultValue "Registry::HKEY_CLASSES_ROOT\.$ext"
|
|
120
|
+
if ($defaultProg -and $defaultProg -ne $userChoice) { Add-ProgId ([string]$defaultProg) $true }
|
|
121
|
+
|
|
122
|
+
$hkcr = Get-Item "Registry::HKEY_CLASSES_ROOT\.$ext\OpenWithProgids" -ErrorAction SilentlyContinue
|
|
123
|
+
if ($hkcr) { foreach ($p in $hkcr.Property) { Add-ProgId ([string]$p) $false } }
|
|
124
|
+
|
|
125
|
+
$hkcu = Get-Item "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.$ext\OpenWithProgids" -ErrorAction SilentlyContinue
|
|
126
|
+
if ($hkcu) { foreach ($p in $hkcu.Property) { Add-ProgId ([string]$p) $false } }
|
|
127
|
+
|
|
128
|
+
$owList = Get-Item "Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.$ext\OpenWithList" -ErrorAction SilentlyContinue
|
|
129
|
+
if ($owList) {
|
|
130
|
+
foreach ($p in $owList.Property) {
|
|
131
|
+
if ($p -eq 'MRUList') { continue }
|
|
132
|
+
$exeName = [string]$owList.GetValue($p)
|
|
133
|
+
if ([string]::IsNullOrWhiteSpace($exeName)) { continue }
|
|
134
|
+
if ($exeName -match '!') {
|
|
135
|
+
Add-App $exeName $null $false
|
|
136
|
+
} else {
|
|
137
|
+
$cmd = Get-DefaultValue "Registry::HKEY_CLASSES_ROOT\Applications\$exeName\shell\open\command"
|
|
138
|
+
$exe = Parse-ExePath ([string]$cmd)
|
|
139
|
+
if (-not $exe) { $exe = $exeName }
|
|
140
|
+
$name = Get-DefaultValue "Registry::HKEY_CLASSES_ROOT\Applications\$exeName"
|
|
141
|
+
Add-App $exe ([string]$name) $false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# No explicit default handler: fall back to the first candidate so the
|
|
147
|
+
# "Open" button still shows a real app logo instead of a generic icon.
|
|
148
|
+
if (-not $script:defaultApp -and $script:apps.Count -gt 0) {
|
|
149
|
+
$script:defaultApp = $script:apps[0]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@{ apps = $script:apps; default = $script:defaultApp } | ConvertTo-Json -Depth 4
|
|
153
|
+
PWSH
|
|
154
|
+
|
|
155
|
+
# Extracts the associated icon of an app as base64 PNG on stdout. Accepts
|
|
156
|
+
# either a desktop .exe path or a packaged (UWP) AUMID; an AUMID is
|
|
157
|
+
# resolved to the package's executable via its AppxManifest before the
|
|
158
|
+
# icon is read. `$Target` is injected as a single-quoted literal (embedded
|
|
159
|
+
# single quotes are doubled) so a value cannot break out of the script.
|
|
160
|
+
ICON_SCRIPT = <<~'PWSH'
|
|
161
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
162
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
163
|
+
$Target = '__CLACKY_TARGET__'
|
|
164
|
+
|
|
165
|
+
$exe = $null
|
|
166
|
+
if ($Target -match '\.exe$') {
|
|
167
|
+
$exe = $Target
|
|
168
|
+
} else {
|
|
169
|
+
$parts = $Target -split '!', 2
|
|
170
|
+
$family = $parts[0]
|
|
171
|
+
$appId = if ($parts.Count -gt 1) { $parts[1] } else { $null }
|
|
172
|
+
$pkg = Get-AppxPackage | Where-Object { $_.PackageFamilyName -eq $family } | Select-Object -First 1
|
|
173
|
+
if ($pkg) {
|
|
174
|
+
$manifest = Join-Path $pkg.InstallLocation 'AppxManifest.xml'
|
|
175
|
+
[xml]$xml = Get-Content $manifest
|
|
176
|
+
if ($xml -and $xml.Package -and $xml.Package.Applications) {
|
|
177
|
+
$app = $null
|
|
178
|
+
foreach ($a in $xml.Package.Applications.Application) {
|
|
179
|
+
if ($appId -and $a.Id -eq $appId) { $app = $a; break }
|
|
180
|
+
}
|
|
181
|
+
if (-not $app) { $app = $xml.Package.Applications.Application | Select-Object -First 1 }
|
|
182
|
+
if ($app -and $app.Executable) { $exe = Join-Path $pkg.InstallLocation $app.Executable }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (-not $exe -or -not (Test-Path $exe)) { exit 1 }
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
Add-Type -AssemblyName System.Drawing
|
|
190
|
+
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($exe)
|
|
191
|
+
if (-not $icon) { exit 1 }
|
|
192
|
+
$bmp = $icon.ToBitmap()
|
|
193
|
+
$ms = New-Object System.IO.MemoryStream
|
|
194
|
+
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
195
|
+
[Convert]::ToBase64String($ms.ToArray())
|
|
196
|
+
} catch {
|
|
197
|
+
exit 1
|
|
198
|
+
}
|
|
199
|
+
PWSH
|
|
200
|
+
|
|
201
|
+
DETECT_MUTEX = Mutex.new
|
|
202
|
+
|
|
203
|
+
# @param ext [String] file extension, with or without leading dot
|
|
204
|
+
# @return [Array<Hash>] [{ "name" => ..., "path" => exe path or AUMID }]
|
|
205
|
+
def self.apps_for_ext(ext)
|
|
206
|
+
return [] unless wsl?
|
|
207
|
+
|
|
208
|
+
ext = normalize_ext(ext)
|
|
209
|
+
return [] if ext.empty?
|
|
210
|
+
|
|
211
|
+
data = detect_for_ext(ext)
|
|
212
|
+
data ? data["apps"] : []
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Return the application Windows would use to open `path`.
|
|
216
|
+
# @param path [String] absolute file path
|
|
217
|
+
# @return [Hash, nil] { "name" => ..., "path" => exe path or AUMID }
|
|
218
|
+
def self.default_app_for(path)
|
|
219
|
+
return nil unless wsl?
|
|
220
|
+
|
|
221
|
+
ext = normalize_ext(File.extname(path))
|
|
222
|
+
return nil if ext.empty?
|
|
223
|
+
|
|
224
|
+
data = detect_for_ext(ext)
|
|
225
|
+
data ? data["default"] : nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Open a file with a specific installed application.
|
|
229
|
+
# @param path [String] absolute Linux-side file path
|
|
230
|
+
# @param app [String] app name or path from /api/file/apps
|
|
231
|
+
# @return [Boolean, nil] system() result, nil when the app is unknown
|
|
232
|
+
def self.open_with(path, app)
|
|
233
|
+
return nil unless wsl?
|
|
234
|
+
|
|
235
|
+
app = app.to_s
|
|
236
|
+
return nil if app.empty?
|
|
237
|
+
|
|
238
|
+
target = resolve_target(path, app)
|
|
239
|
+
return nil unless target
|
|
240
|
+
|
|
241
|
+
if aumid?(target)
|
|
242
|
+
# explorer.exe hands the launch off to the shell and always exits
|
|
243
|
+
# non-zero, so the exit status cannot signal success — fire-and-forget.
|
|
244
|
+
system("explorer.exe", "shell:appsFolder\\#{target}")
|
|
245
|
+
true
|
|
246
|
+
else
|
|
247
|
+
win_path = Utils::EnvironmentDetector.linux_to_win_path(path)
|
|
248
|
+
system("cmd.exe", "/c", "start", "", target, win_path)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Convert an app's icon to a cached PNG. Accepts a desktop .exe path or a
|
|
253
|
+
# packaged (UWP) AUMID; the AUMID is resolved to the package exe inside
|
|
254
|
+
# the PowerShell script. Answers nil when no icon can be extracted.
|
|
255
|
+
# @param target [String] Windows .exe path or AppUserModelID
|
|
256
|
+
# @return [String, nil] path to the cached PNG file
|
|
257
|
+
def self.icon_png(target)
|
|
258
|
+
return nil unless wsl?
|
|
259
|
+
|
|
260
|
+
target = target.to_s
|
|
261
|
+
return nil if target.empty?
|
|
262
|
+
|
|
263
|
+
cache_dir = File.join(Dir.home, ".clacky", "cache", "app-icons")
|
|
264
|
+
cache = File.join(cache_dir, Digest::MD5.hexdigest(target) + ".png")
|
|
265
|
+
return cache if File.exist?(cache)
|
|
266
|
+
|
|
267
|
+
out, _err, status = run_encoded(ICON_SCRIPT.sub("__CLACKY_TARGET__", target.gsub("'", "''")))
|
|
268
|
+
return nil unless status.success?
|
|
269
|
+
b64 = out.strip
|
|
270
|
+
return nil if b64.empty?
|
|
271
|
+
|
|
272
|
+
require "fileutils"
|
|
273
|
+
FileUtils.mkdir_p(cache_dir)
|
|
274
|
+
File.binwrite(cache, Base64.decode64(b64))
|
|
275
|
+
File.exist?(cache) ? cache : nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.wsl?
|
|
279
|
+
Utils::EnvironmentDetector.os_type == :wsl
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Strips to [a-z0-9] so the value can never break out of the
|
|
283
|
+
# single-quoted `$Ext` literal inside DETECT_SCRIPT.
|
|
284
|
+
def self.normalize_ext(ext)
|
|
285
|
+
ext.to_s.downcase.sub(/\A\./, "").gsub(/[^a-z0-9]/, "")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.detect_for_ext(ext)
|
|
289
|
+
@detect_cache ||= {}
|
|
290
|
+
return @detect_cache[ext] if @detect_cache.key?(ext)
|
|
291
|
+
|
|
292
|
+
# powershell.exe takes 1-3s to spawn on WSL; the mutex keeps concurrent
|
|
293
|
+
# /api/file/apps + /api/file/default-app requests from launching a
|
|
294
|
+
# duplicate process for the same extension.
|
|
295
|
+
DETECT_MUTEX.synchronize do
|
|
296
|
+
return @detect_cache[ext] if @detect_cache.key?(ext)
|
|
297
|
+
|
|
298
|
+
out, _err, status = run_encoded(DETECT_SCRIPT.sub("__CLACKY_EXT__", ext))
|
|
299
|
+
parsed = begin
|
|
300
|
+
JSON.parse(out.sub(/\A\xEF\xBB\xBF/, ""))
|
|
301
|
+
rescue StandardError
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
@detect_cache[ext] = status.success? && parsed.is_a?(Hash) ? parsed : nil
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# The app must match an entry returned by /api/file/apps for this file
|
|
309
|
+
# type, mirroring MacAppDetector's all_apps whitelist — a caller
|
|
310
|
+
# supplied exe path or AUMID is never trusted directly.
|
|
311
|
+
def self.resolve_target(path, app)
|
|
312
|
+
ext = normalize_ext(File.extname(path))
|
|
313
|
+
data = detect_for_ext(ext)
|
|
314
|
+
return nil unless data
|
|
315
|
+
|
|
316
|
+
match = data["apps"].find { |a| a["name"] == app || a["path"] == app }
|
|
317
|
+
match && match["path"]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def self.aumid?(target)
|
|
321
|
+
target.to_s.include?("!") && target.to_s !~ /\.exe\z/i
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def self.run_encoded(script)
|
|
325
|
+
encoded = Base64.strict_encode64(script.encode("UTF-16LE"))
|
|
326
|
+
out, err, status = Open3.capture3("powershell.exe", "-NoProfile", "-EncodedCommand", encoded)
|
|
327
|
+
# The script forces UTF-8 output, but capture3 tags the bytes with the
|
|
328
|
+
# host locale (US-ASCII under LANG=C), which makes JSON.parse choke on
|
|
329
|
+
# non-ASCII app names — retag before the caller parses.
|
|
330
|
+
[Clacky::Utils::Encoding.cmd_to_utf8(out, source_encoding: "UTF-8"), err, status]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
data/lib/clacky/version.rb
CHANGED