pi-agent-browser-native 0.2.47 → 0.2.49
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/CHANGELOG.md +63 -19
- package/README.md +52 -19
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/ARCHITECTURE.md +10 -10
- package/docs/COMMAND_REFERENCE.md +35 -21
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +46 -26
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +35 -106
- package/docs/TOOL_CONTRACT.md +23 -21
- package/package.json +12 -8
- package/scripts/agent-browser-capability-baseline.mjs +6 -3
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +19 -17
- package/scripts/platform-smoke.mjs +1 -1
- package/extensions/agent-browser/index.ts +0 -952
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -209
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -451
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -257
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -912
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -512
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -1481
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -252
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -721
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Discover installed Electron desktop applications without invoking upstream agent-browser.
|
|
3
|
-
* Responsibilities: Scan bounded macOS app bundles and Linux .desktop launchers, apply Electron framework evidence gates, and return small platform-tagged app metadata.
|
|
4
|
-
* Scope: Discovery only; launch, cleanup, status, and CDP attachment live in later Electron lifecycle work items.
|
|
5
|
-
* Usage: Called by the agent_browser top-level electron.list shorthand and directly by tests through parameterized scan locations.
|
|
6
|
-
* Invariants/Assumptions: Discovery is best-effort, missing scan roots are ignored, malformed .desktop files are skipped, and results are capped before they reach model-visible output.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { constants as fsConstants } from "node:fs";
|
|
10
|
-
import { access, readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
13
|
-
|
|
14
|
-
import { pathExists } from "../fs-utils.js";
|
|
15
|
-
|
|
16
|
-
export const ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS = 50;
|
|
17
|
-
export const ELECTRON_DISCOVERY_MAX_RESULTS = 200;
|
|
18
|
-
|
|
19
|
-
const LINUX_ELECTRON_CANDIDATE_MAX_DEPTH = 7;
|
|
20
|
-
const LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES = 5_000;
|
|
21
|
-
const LINUX_NON_EXECUTABLE_CANDIDATE_EXTENSIONS = new Set([".asar", ".dat", ".desktop", ".json", ".md", ".pak", ".png", ".so", ".txt"]);
|
|
22
|
-
|
|
23
|
-
export type ElectronDiscoveryPlatform = "darwin" | "linux" | "win32";
|
|
24
|
-
|
|
25
|
-
export interface ElectronAppSensitivity {
|
|
26
|
-
categories: string[];
|
|
27
|
-
level: "likely-sensitive";
|
|
28
|
-
reason: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ElectronAppDiscovery {
|
|
32
|
-
appPath?: string;
|
|
33
|
-
bundleId?: string;
|
|
34
|
-
comment?: string;
|
|
35
|
-
desktopId?: string;
|
|
36
|
-
executablePath: string;
|
|
37
|
-
icon?: string;
|
|
38
|
-
name: string;
|
|
39
|
-
packageSource?: "desktop" | "flatpak" | "snap";
|
|
40
|
-
platform: ElectronDiscoveryPlatform;
|
|
41
|
-
sensitivity?: ElectronAppSensitivity;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ElectronDiscoveryScanLocations {
|
|
45
|
-
darwinApplicationDirectories?: string[];
|
|
46
|
-
flatpakSystemAppDirectory?: string;
|
|
47
|
-
flatpakUserAppDirectory?: string;
|
|
48
|
-
homeDir?: string;
|
|
49
|
-
linuxDesktopDirectories?: string[];
|
|
50
|
-
pathEnv?: string;
|
|
51
|
-
snapBinDirectory?: string;
|
|
52
|
-
snapMountDirectory?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface DiscoverElectronAppsOptions {
|
|
56
|
-
locations?: ElectronDiscoveryScanLocations;
|
|
57
|
-
maxResults?: number;
|
|
58
|
-
platform?: NodeJS.Platform | ElectronDiscoveryPlatform;
|
|
59
|
-
query?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ElectronDiscoveryResult {
|
|
63
|
-
apps: ElectronAppDiscovery[];
|
|
64
|
-
maxResults: number;
|
|
65
|
-
omittedCount: number;
|
|
66
|
-
platform: ElectronDiscoveryPlatform | "unsupported";
|
|
67
|
-
query?: string;
|
|
68
|
-
skippedCount?: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface ResolvedElectronDiscoveryLocations {
|
|
72
|
-
darwinApplicationDirectories: string[];
|
|
73
|
-
flatpakSystemAppDirectory: string;
|
|
74
|
-
flatpakUserAppDirectory: string;
|
|
75
|
-
homeDir: string;
|
|
76
|
-
linuxDesktopDirectories: string[];
|
|
77
|
-
pathEnv: string;
|
|
78
|
-
snapBinDirectory: string;
|
|
79
|
-
snapMountDirectory: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface LinuxDesktopEntry {
|
|
83
|
-
comment?: string;
|
|
84
|
-
desktopId: string;
|
|
85
|
-
exec: string;
|
|
86
|
-
filePath: string;
|
|
87
|
-
icon?: string;
|
|
88
|
-
name: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface LinuxExecutableResolution {
|
|
92
|
-
executablePath: string;
|
|
93
|
-
packageSource: "desktop" | "flatpak" | "snap";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const ELECTRON_SENSITIVE_APP_CATEGORY_PATTERNS: Array<{ category: string; patterns: RegExp[] }> = [
|
|
97
|
-
{ category: "notes", patterns: [/\bobsidian\b/i, /\bnotion\b/i, /\blogseq\b/i] },
|
|
98
|
-
{ category: "chat", patterns: [/\bslack\b/i, /\bdiscord\b/i, /\bteams\b/i, /\bsignal\b/i, /\btelegram\b/i, /\bwhatsapp\b/i] },
|
|
99
|
-
{ category: "mail", patterns: [/\bmail\b/i, /\boutlook\b/i, /\bthunderbird\b/i, /\bspark\b/i, /\bproton[- ]?mail\b/i] },
|
|
100
|
-
{ category: "developer-workspace", patterns: [/\bvisual studio code\b/i, /\bvs ?code\b/i, /\bcode - insiders\b/i, /^code$/i, /\bcursor\b/i, /\bwindsurf\b/i] },
|
|
101
|
-
{ category: "passwords-auth", patterns: [/\b1password\b/i, /\bbitwarden\b/i, /\blastpass\b/i, /\bdashlane\b/i, /\bauthy\b/i, /\bauthenticator\b/i, /\bkeepass\b/i] },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
function normalizeSensitivityValue(value: string | undefined): string | undefined {
|
|
105
|
-
return value?.trim().replace(/[_./\\-]+/g, " ").replace(/\s+/g, " ") || undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function getElectronAppSensitivity(app: ElectronAppDiscovery): ElectronAppSensitivity | undefined {
|
|
109
|
-
const metadataValues = [app.name, app.bundleId, app.desktopId, app.appPath, app.executablePath]
|
|
110
|
-
.map(normalizeSensitivityValue)
|
|
111
|
-
.filter((value): value is string => value !== undefined);
|
|
112
|
-
const categories = ELECTRON_SENSITIVE_APP_CATEGORY_PATTERNS
|
|
113
|
-
.filter(({ patterns }) => metadataValues.some((value) => patterns.some((pattern) => pattern.test(value))))
|
|
114
|
-
.map(({ category }) => category);
|
|
115
|
-
if (categories.length === 0) return undefined;
|
|
116
|
-
return {
|
|
117
|
-
categories: [...new Set(categories)].sort(),
|
|
118
|
-
level: "likely-sensitive",
|
|
119
|
-
reason: "App name, bundle id, desktop id, or path matched common private-data app patterns; discovery still does not enforce policy.",
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function annotateElectronAppSensitivity(app: ElectronAppDiscovery): ElectronAppDiscovery {
|
|
124
|
-
const sensitivity = getElectronAppSensitivity(app);
|
|
125
|
-
return sensitivity ? { ...app, sensitivity } : app;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function normalizeMaxResults(maxResults: number | undefined): number {
|
|
129
|
-
if (typeof maxResults !== "number" || !Number.isInteger(maxResults) || maxResults <= 0) {
|
|
130
|
-
return ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS;
|
|
131
|
-
}
|
|
132
|
-
return Math.min(maxResults, ELECTRON_DISCOVERY_MAX_RESULTS);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function resolveLocations(locations: ElectronDiscoveryScanLocations | undefined): ResolvedElectronDiscoveryLocations {
|
|
136
|
-
const homeDir = locations?.homeDir ?? homedir();
|
|
137
|
-
return {
|
|
138
|
-
darwinApplicationDirectories: locations?.darwinApplicationDirectories ?? ["/Applications", join(homeDir, "Applications")],
|
|
139
|
-
flatpakSystemAppDirectory: locations?.flatpakSystemAppDirectory ?? "/var/lib/flatpak/app",
|
|
140
|
-
flatpakUserAppDirectory: locations?.flatpakUserAppDirectory ?? join(homeDir, ".local", "share", "flatpak", "app"),
|
|
141
|
-
homeDir,
|
|
142
|
-
linuxDesktopDirectories: locations?.linuxDesktopDirectories ?? [
|
|
143
|
-
join(homeDir, ".local", "share", "applications"),
|
|
144
|
-
"/usr/share/applications",
|
|
145
|
-
"/var/lib/snapd/desktop/applications",
|
|
146
|
-
join(homeDir, ".local", "share", "flatpak", "exports", "share", "applications"),
|
|
147
|
-
"/var/lib/flatpak/exports/share/applications",
|
|
148
|
-
],
|
|
149
|
-
pathEnv: locations?.pathEnv ?? process.env.PATH ?? "",
|
|
150
|
-
snapBinDirectory: locations?.snapBinDirectory ?? "/snap/bin",
|
|
151
|
-
snapMountDirectory: locations?.snapMountDirectory ?? "/snap",
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function isDirectory(path: string): Promise<boolean> {
|
|
156
|
-
try {
|
|
157
|
-
return (await stat(path)).isDirectory();
|
|
158
|
-
} catch {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function isFile(path: string): Promise<boolean> {
|
|
164
|
-
try {
|
|
165
|
-
return (await stat(path)).isFile();
|
|
166
|
-
} catch {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function isExecutableFile(path: string): Promise<boolean> {
|
|
172
|
-
try {
|
|
173
|
-
const metadata = await stat(path);
|
|
174
|
-
if (!metadata.isFile()) return false;
|
|
175
|
-
await access(path, fsConstants.X_OK);
|
|
176
|
-
return true;
|
|
177
|
-
} catch {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function resolveRealPath(path: string): Promise<string> {
|
|
183
|
-
try {
|
|
184
|
-
return await realpath(path);
|
|
185
|
-
} catch {
|
|
186
|
-
return path;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function escapeRegExp(value: string): string {
|
|
191
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function decodeXmlEntities(value: string): string {
|
|
195
|
-
return value
|
|
196
|
-
.replaceAll("&", "&")
|
|
197
|
-
.replaceAll("<", "<")
|
|
198
|
-
.replaceAll(">", ">")
|
|
199
|
-
.replaceAll(""", '"')
|
|
200
|
-
.replaceAll("'", "'");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function readPlistString(plist: string, key: string): string | undefined {
|
|
204
|
-
const pattern = new RegExp(`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`, "i");
|
|
205
|
-
const match = pattern.exec(plist);
|
|
206
|
-
return match ? decodeXmlEntities(match[1]?.trim() ?? "") : undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function readMacInfoPlist(appPath: string): Promise<Record<string, string>> {
|
|
210
|
-
try {
|
|
211
|
-
const plist = await readFile(join(appPath, "Contents", "Info.plist"), "utf8");
|
|
212
|
-
return {
|
|
213
|
-
CFBundleDisplayName: readPlistString(plist, "CFBundleDisplayName") ?? "",
|
|
214
|
-
CFBundleExecutable: readPlistString(plist, "CFBundleExecutable") ?? "",
|
|
215
|
-
CFBundleIdentifier: readPlistString(plist, "CFBundleIdentifier") ?? "",
|
|
216
|
-
CFBundleName: readPlistString(plist, "CFBundleName") ?? "",
|
|
217
|
-
};
|
|
218
|
-
} catch {
|
|
219
|
-
return {};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function resolveMacExecutablePath(appPath: string, executableName: string | undefined, fallbackName: string): Promise<string | undefined> {
|
|
224
|
-
const macOsDirectory = join(appPath, "Contents", "MacOS");
|
|
225
|
-
if (executableName && executableName.trim().length > 0) {
|
|
226
|
-
const executablePath = join(macOsDirectory, executableName);
|
|
227
|
-
return await pathExists(executablePath) ? executablePath : undefined;
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
const entries = await readdir(macOsDirectory, { withFileTypes: true });
|
|
231
|
-
const exact = entries.find((entry) => entry.isFile() && entry.name === fallbackName);
|
|
232
|
-
const candidate = exact ?? entries.find((entry) => entry.isFile());
|
|
233
|
-
return candidate ? join(macOsDirectory, candidate.name) : undefined;
|
|
234
|
-
} catch {
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export async function inspectDarwinApp(appPath: string): Promise<ElectronAppDiscovery | undefined> {
|
|
240
|
-
const frameworkPath = join(appPath, "Contents", "Frameworks", "Electron Framework.framework");
|
|
241
|
-
const resourcesPath = join(appPath, "Contents", "Resources");
|
|
242
|
-
const hasElectronFramework = await isDirectory(frameworkPath);
|
|
243
|
-
const hasAppPayload = await pathExists(join(resourcesPath, "app.asar")) || await isDirectory(join(resourcesPath, "app"));
|
|
244
|
-
if (!hasElectronFramework || !hasAppPayload) return undefined;
|
|
245
|
-
|
|
246
|
-
const info = await readMacInfoPlist(appPath);
|
|
247
|
-
const appDirectoryName = basename(appPath, ".app");
|
|
248
|
-
const executablePath = await resolveMacExecutablePath(appPath, info.CFBundleExecutable, appDirectoryName);
|
|
249
|
-
if (!executablePath) return undefined;
|
|
250
|
-
const name = info.CFBundleDisplayName || info.CFBundleName || appDirectoryName;
|
|
251
|
-
return {
|
|
252
|
-
appPath,
|
|
253
|
-
bundleId: info.CFBundleIdentifier || undefined,
|
|
254
|
-
executablePath,
|
|
255
|
-
name,
|
|
256
|
-
platform: "darwin",
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function discoverDarwinApps(locations: ResolvedElectronDiscoveryLocations): Promise<{ apps: ElectronAppDiscovery[]; skippedCount: number }> {
|
|
261
|
-
const apps: ElectronAppDiscovery[] = [];
|
|
262
|
-
let skippedCount = 0;
|
|
263
|
-
for (const directory of locations.darwinApplicationDirectories) {
|
|
264
|
-
let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
|
|
265
|
-
try {
|
|
266
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
267
|
-
} catch {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
for (const entry of entries) {
|
|
271
|
-
if (!entry.name.endsWith(".app")) continue;
|
|
272
|
-
const appPath = join(directory, entry.name);
|
|
273
|
-
try {
|
|
274
|
-
const app = await inspectDarwinApp(appPath);
|
|
275
|
-
if (app) apps.push(app);
|
|
276
|
-
} catch {
|
|
277
|
-
skippedCount += 1;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return { apps, skippedCount };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function unescapeDesktopValue(value: string): string {
|
|
285
|
-
return value
|
|
286
|
-
.replace(/\\s/g, " ")
|
|
287
|
-
.replace(/\\n/g, "\n")
|
|
288
|
-
.replace(/\\r/g, "\r")
|
|
289
|
-
.replace(/\\t/g, "\t")
|
|
290
|
-
.replace(/\\\\/g, "\\");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function parseDesktopBoolean(value: string | undefined): boolean {
|
|
294
|
-
return value?.trim().toLowerCase() === "true";
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function parseDesktopFile(text: string, filePath: string): LinuxDesktopEntry | undefined {
|
|
298
|
-
const fields = new Map<string, string>();
|
|
299
|
-
let inDesktopEntry = false;
|
|
300
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
301
|
-
const line = rawLine.trim();
|
|
302
|
-
if (line.length === 0 || line.startsWith("#")) continue;
|
|
303
|
-
if (line.startsWith("[") && line.endsWith("]")) {
|
|
304
|
-
inDesktopEntry = line === "[Desktop Entry]";
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (!inDesktopEntry) continue;
|
|
308
|
-
const separatorIndex = line.indexOf("=");
|
|
309
|
-
if (separatorIndex <= 0) continue;
|
|
310
|
-
const key = line.slice(0, separatorIndex);
|
|
311
|
-
const value = unescapeDesktopValue(line.slice(separatorIndex + 1));
|
|
312
|
-
fields.set(key, value);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (fields.get("Type") !== "Application") return undefined;
|
|
316
|
-
if (parseDesktopBoolean(fields.get("NoDisplay")) || parseDesktopBoolean(fields.get("Hidden"))) return undefined;
|
|
317
|
-
const exec = fields.get("Exec")?.trim();
|
|
318
|
-
if (!exec) return undefined;
|
|
319
|
-
const desktopId = basename(filePath, ".desktop");
|
|
320
|
-
return {
|
|
321
|
-
comment: fields.get("Comment") || undefined,
|
|
322
|
-
desktopId,
|
|
323
|
-
exec,
|
|
324
|
-
filePath,
|
|
325
|
-
icon: fields.get("Icon") || undefined,
|
|
326
|
-
name: fields.get("Name") || desktopId,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function stripDesktopExecFieldCodes(exec: string): string {
|
|
331
|
-
const placeholder = "\u0000PERCENT\u0000";
|
|
332
|
-
return exec
|
|
333
|
-
.replaceAll("%%", placeholder)
|
|
334
|
-
.replace(/%[A-Za-z]/g, "")
|
|
335
|
-
.replaceAll(placeholder, "%");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function tokenizeDesktopExec(exec: string): string[] {
|
|
339
|
-
const tokens: string[] = [];
|
|
340
|
-
let current = "";
|
|
341
|
-
let quote: '"' | "'" | undefined;
|
|
342
|
-
let escaped = false;
|
|
343
|
-
for (const char of stripDesktopExecFieldCodes(exec)) {
|
|
344
|
-
if (escaped) {
|
|
345
|
-
current += char;
|
|
346
|
-
escaped = false;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (char === "\\") {
|
|
350
|
-
escaped = true;
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (quote) {
|
|
354
|
-
if (char === quote) {
|
|
355
|
-
quote = undefined;
|
|
356
|
-
} else {
|
|
357
|
-
current += char;
|
|
358
|
-
}
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (char === '"' || char === "'") {
|
|
362
|
-
quote = char;
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
if (/\s/.test(char)) {
|
|
366
|
-
if (current.length > 0) {
|
|
367
|
-
tokens.push(current);
|
|
368
|
-
current = "";
|
|
369
|
-
}
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
current += char;
|
|
373
|
-
}
|
|
374
|
-
if (escaped) current += "\\";
|
|
375
|
-
if (current.length > 0) tokens.push(current);
|
|
376
|
-
return tokens;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function stripEnvLauncher(tokens: string[]): string[] {
|
|
380
|
-
if (tokens.length === 0 || basename(tokens[0] ?? "") !== "env") return tokens;
|
|
381
|
-
let index = 1;
|
|
382
|
-
while (index < tokens.length) {
|
|
383
|
-
const token = tokens[index] ?? "";
|
|
384
|
-
if (token.startsWith("-")) {
|
|
385
|
-
index += token === "-u" || token === "--unset" || token === "--chdir" ? 2 : 1;
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) {
|
|
389
|
-
index += 1;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
return tokens.slice(index);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function directoryContainsChromePak(directory: string): Promise<boolean> {
|
|
398
|
-
try {
|
|
399
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
400
|
-
return entries.some((entry) => entry.isFile() && /^chrome_.*\.pak$/i.test(entry.name));
|
|
401
|
-
} catch {
|
|
402
|
-
return false;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export async function hasLinuxElectronEvidence(executablePath: string): Promise<boolean> {
|
|
407
|
-
const resolvedExecutablePath = await resolveRealPath(executablePath);
|
|
408
|
-
if (!await isExecutableFile(resolvedExecutablePath)) return false;
|
|
409
|
-
const executableDirectory = dirname(resolvedExecutablePath);
|
|
410
|
-
if (!await directoryContainsChromePak(executableDirectory)) return false;
|
|
411
|
-
const resourceBases = [executableDirectory, dirname(executableDirectory)];
|
|
412
|
-
for (const base of resourceBases) {
|
|
413
|
-
const resourcesDirectory = join(base, "resources");
|
|
414
|
-
if (await pathExists(join(resourcesDirectory, "app.asar")) || await isDirectory(join(resourcesDirectory, "app"))) {
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async function findExecutableInPath(command: string, pathEnv: string): Promise<string | undefined> {
|
|
422
|
-
if (command.includes("/")) return undefined;
|
|
423
|
-
for (const directory of pathEnv.split(":")) {
|
|
424
|
-
if (directory.length === 0) continue;
|
|
425
|
-
const candidate = join(directory, command);
|
|
426
|
-
if (await isFile(candidate)) return candidate;
|
|
427
|
-
}
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function pathIsWithin(path: string, parent: string): boolean {
|
|
432
|
-
const relativePath = relative(resolve(parent), resolve(path));
|
|
433
|
-
return relativePath.length === 0 || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function isLikelyExecutableCandidate(path: string): boolean {
|
|
437
|
-
return !LINUX_NON_EXECUTABLE_CANDIDATE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
async function findElectronBinaryUnder(root: string, preferredNames: string[]): Promise<string | undefined> {
|
|
441
|
-
if (!await isDirectory(root)) return undefined;
|
|
442
|
-
const preferredNameSet = new Set(preferredNames.filter((name) => name.length > 0));
|
|
443
|
-
const allFileCandidates: string[] = [];
|
|
444
|
-
let visitedEntries = 0;
|
|
445
|
-
|
|
446
|
-
async function visit(directory: string, depth: number): Promise<string | undefined> {
|
|
447
|
-
if (depth > LINUX_ELECTRON_CANDIDATE_MAX_DEPTH || visitedEntries >= LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES) return undefined;
|
|
448
|
-
let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
|
|
449
|
-
try {
|
|
450
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
451
|
-
} catch {
|
|
452
|
-
return undefined;
|
|
453
|
-
}
|
|
454
|
-
for (const entry of entries) {
|
|
455
|
-
visitedEntries += 1;
|
|
456
|
-
if (visitedEntries > LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES) return undefined;
|
|
457
|
-
const path = join(directory, entry.name);
|
|
458
|
-
if (entry.isDirectory()) {
|
|
459
|
-
const found = await visit(path, depth + 1);
|
|
460
|
-
if (found) return found;
|
|
461
|
-
} else if (entry.isFile() && isLikelyExecutableCandidate(path)) {
|
|
462
|
-
if (preferredNameSet.has(entry.name) && await hasLinuxElectronEvidence(path)) return await resolveRealPath(path);
|
|
463
|
-
allFileCandidates.push(path);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const preferredMatch = await visit(root, 0);
|
|
470
|
-
if (preferredMatch) return preferredMatch;
|
|
471
|
-
for (const candidate of allFileCandidates) {
|
|
472
|
-
if (await hasLinuxElectronEvidence(candidate)) return await resolveRealPath(candidate);
|
|
473
|
-
}
|
|
474
|
-
return undefined;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function getSnapCandidateNames(commandName: string, desktopId: string): string[] {
|
|
478
|
-
const names = [commandName, commandName.split(".")[0] ?? "", desktopId.split(".")[0] ?? ""];
|
|
479
|
-
return [...new Set(names.filter((name) => name.length > 0))];
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async function resolveSnapExecutable(commandPath: string, entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<string | undefined> {
|
|
483
|
-
const commandName = basename(commandPath);
|
|
484
|
-
for (const snapName of getSnapCandidateNames(commandName, entry.desktopId)) {
|
|
485
|
-
const root = join(locations.snapMountDirectory, snapName, "current");
|
|
486
|
-
const candidate = await findElectronBinaryUnder(root, [commandName, commandName.split(".").at(-1) ?? commandName]);
|
|
487
|
-
if (candidate) return candidate;
|
|
488
|
-
}
|
|
489
|
-
return undefined;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function getFlatpakAppId(tokens: string[], desktopId: string): string {
|
|
493
|
-
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
494
|
-
const token = tokens[index] ?? "";
|
|
495
|
-
if (!token.startsWith("-") && token.includes(".") && token !== "flatpak") return token;
|
|
496
|
-
}
|
|
497
|
-
return desktopId;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function getFlatpakCommandName(tokens: string[]): string | undefined {
|
|
501
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
502
|
-
const token = tokens[index] ?? "";
|
|
503
|
-
if (token.startsWith("--command=")) return token.slice("--command=".length);
|
|
504
|
-
if (token === "--command") return tokens[index + 1];
|
|
505
|
-
}
|
|
506
|
-
return undefined;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function getFlatpakRoots(entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): string[] {
|
|
510
|
-
const userExportDirectory = join(locations.homeDir, ".local", "share", "flatpak", "exports", "share", "applications");
|
|
511
|
-
const systemExportDirectory = "/var/lib/flatpak/exports/share/applications";
|
|
512
|
-
if (pathIsWithin(entry.filePath, userExportDirectory)) {
|
|
513
|
-
return [locations.flatpakUserAppDirectory, locations.flatpakSystemAppDirectory];
|
|
514
|
-
}
|
|
515
|
-
if (pathIsWithin(entry.filePath, systemExportDirectory)) {
|
|
516
|
-
return [locations.flatpakSystemAppDirectory, locations.flatpakUserAppDirectory];
|
|
517
|
-
}
|
|
518
|
-
return [locations.flatpakUserAppDirectory, locations.flatpakSystemAppDirectory];
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function resolveFlatpakExecutable(tokens: string[], entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<string | undefined> {
|
|
522
|
-
const appId = getFlatpakAppId(tokens, entry.desktopId);
|
|
523
|
-
const commandName = getFlatpakCommandName(tokens);
|
|
524
|
-
const preferredNames = commandName ? [commandName] : [];
|
|
525
|
-
for (const root of getFlatpakRoots(entry, locations)) {
|
|
526
|
-
const filesRoot = join(root, appId, "current", "active", "files");
|
|
527
|
-
const candidate = await findElectronBinaryUnder(filesRoot, preferredNames);
|
|
528
|
-
if (candidate) return candidate;
|
|
529
|
-
}
|
|
530
|
-
return undefined;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async function resolveLinuxExecutable(entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<LinuxExecutableResolution | undefined> {
|
|
534
|
-
const tokens = stripEnvLauncher(tokenizeDesktopExec(entry.exec));
|
|
535
|
-
const executableToken = tokens[0];
|
|
536
|
-
if (!executableToken) return undefined;
|
|
537
|
-
|
|
538
|
-
if (basename(executableToken) === "flatpak") {
|
|
539
|
-
const executablePath = await resolveFlatpakExecutable(tokens, entry, locations);
|
|
540
|
-
return executablePath ? { executablePath, packageSource: "flatpak" } : undefined;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
let executablePath = executableToken;
|
|
544
|
-
if (!isAbsolute(executablePath)) {
|
|
545
|
-
const pathExecutable = await findExecutableInPath(executablePath, locations.pathEnv);
|
|
546
|
-
if (!pathExecutable) return undefined;
|
|
547
|
-
executablePath = pathExecutable;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (pathIsWithin(executablePath, locations.snapBinDirectory)) {
|
|
551
|
-
const snapExecutable = await resolveSnapExecutable(executablePath, entry, locations);
|
|
552
|
-
return snapExecutable ? { executablePath: snapExecutable, packageSource: "snap" } : undefined;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return { executablePath: await resolveRealPath(executablePath), packageSource: "desktop" };
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function inspectLinuxDesktopFile(filePath: string, locations: ResolvedElectronDiscoveryLocations): Promise<ElectronAppDiscovery | undefined> {
|
|
559
|
-
let text: string;
|
|
560
|
-
try {
|
|
561
|
-
text = await readFile(filePath, "utf8");
|
|
562
|
-
} catch {
|
|
563
|
-
return undefined;
|
|
564
|
-
}
|
|
565
|
-
const entry = parseDesktopFile(text, filePath);
|
|
566
|
-
if (!entry) return undefined;
|
|
567
|
-
const resolution = await resolveLinuxExecutable(entry, locations);
|
|
568
|
-
if (!resolution) return undefined;
|
|
569
|
-
if (!await hasLinuxElectronEvidence(resolution.executablePath)) return undefined;
|
|
570
|
-
return {
|
|
571
|
-
comment: entry.comment,
|
|
572
|
-
desktopId: entry.desktopId,
|
|
573
|
-
executablePath: resolution.executablePath,
|
|
574
|
-
icon: entry.icon,
|
|
575
|
-
name: entry.name,
|
|
576
|
-
packageSource: resolution.packageSource,
|
|
577
|
-
platform: "linux",
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function discoverLinuxApps(locations: ResolvedElectronDiscoveryLocations): Promise<{ apps: ElectronAppDiscovery[]; skippedCount: number }> {
|
|
582
|
-
const apps: ElectronAppDiscovery[] = [];
|
|
583
|
-
let skippedCount = 0;
|
|
584
|
-
for (const directory of locations.linuxDesktopDirectories) {
|
|
585
|
-
let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
|
|
586
|
-
try {
|
|
587
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
588
|
-
} catch {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
for (const entry of entries) {
|
|
592
|
-
if (!entry.isFile() || extname(entry.name) !== ".desktop") continue;
|
|
593
|
-
try {
|
|
594
|
-
const app = await inspectLinuxDesktopFile(join(directory, entry.name), locations);
|
|
595
|
-
if (app) apps.push(app);
|
|
596
|
-
} catch {
|
|
597
|
-
skippedCount += 1;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return { apps, skippedCount };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function appMatchesQuery(app: ElectronAppDiscovery, query: string | undefined): boolean {
|
|
605
|
-
if (!query) return true;
|
|
606
|
-
const normalizedQuery = query.toLowerCase();
|
|
607
|
-
const searchableValues = [app.name, app.bundleId, app.appPath, app.executablePath, app.comment, app.desktopId, app.icon, app.packageSource]
|
|
608
|
-
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
609
|
-
.map((value) => value.toLowerCase());
|
|
610
|
-
return searchableValues.some((value) => value.includes(normalizedQuery));
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function dedupeApps(apps: ElectronAppDiscovery[]): ElectronAppDiscovery[] {
|
|
614
|
-
const seen = new Set<string>();
|
|
615
|
-
const deduped: ElectronAppDiscovery[] = [];
|
|
616
|
-
for (const app of apps) {
|
|
617
|
-
const key = app.appPath ?? app.executablePath;
|
|
618
|
-
if (seen.has(key)) continue;
|
|
619
|
-
seen.add(key);
|
|
620
|
-
deduped.push(app);
|
|
621
|
-
}
|
|
622
|
-
return deduped;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function sortApps(apps: ElectronAppDiscovery[]): ElectronAppDiscovery[] {
|
|
626
|
-
return [...apps].sort((left, right) => {
|
|
627
|
-
const nameComparison = left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
628
|
-
return nameComparison === 0 ? left.executablePath.localeCompare(right.executablePath) : nameComparison;
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function findMacAppBundleAncestor(path: string): string | undefined {
|
|
633
|
-
const parts = resolve(path).split(/[\\/]+/);
|
|
634
|
-
const appIndex = parts.findIndex((part) => part.endsWith(".app"));
|
|
635
|
-
return appIndex >= 0 ? parts.slice(0, appIndex + 1).join("/") || "/" : undefined;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function inspectWin32Executable(executablePath: string): Promise<ElectronAppDiscovery | undefined> {
|
|
639
|
-
const resolvedExecutablePath = await resolveRealPath(executablePath);
|
|
640
|
-
if (!await isExecutableFile(resolvedExecutablePath)) return undefined;
|
|
641
|
-
const executableDirectory = dirname(resolvedExecutablePath);
|
|
642
|
-
const resourcesDirectory = join(executableDirectory, "resources");
|
|
643
|
-
const hasAppPayload = await pathExists(join(resourcesDirectory, "app.asar")) || await isDirectory(join(resourcesDirectory, "app"));
|
|
644
|
-
const hasPak = await directoryContainsChromePak(executableDirectory) || await pathExists(join(executableDirectory, "resources.pak"));
|
|
645
|
-
if (!hasAppPayload || !hasPak) return undefined;
|
|
646
|
-
return {
|
|
647
|
-
executablePath: resolvedExecutablePath,
|
|
648
|
-
name: basename(resolvedExecutablePath, extname(resolvedExecutablePath)),
|
|
649
|
-
platform: "win32",
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
export async function inspectElectronExecutablePath(
|
|
654
|
-
executablePath: string,
|
|
655
|
-
platform: NodeJS.Platform | ElectronDiscoveryPlatform = process.platform,
|
|
656
|
-
): Promise<ElectronAppDiscovery | undefined> {
|
|
657
|
-
const resolvedExecutablePath = await resolveRealPath(executablePath);
|
|
658
|
-
if (platform === "darwin") {
|
|
659
|
-
const appPath = findMacAppBundleAncestor(resolvedExecutablePath);
|
|
660
|
-
return appPath ? inspectDarwinApp(appPath) : undefined;
|
|
661
|
-
}
|
|
662
|
-
if (platform === "linux") {
|
|
663
|
-
if (!await hasLinuxElectronEvidence(resolvedExecutablePath)) return undefined;
|
|
664
|
-
return {
|
|
665
|
-
executablePath: resolvedExecutablePath,
|
|
666
|
-
name: basename(resolvedExecutablePath),
|
|
667
|
-
platform: "linux",
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
if (platform === "win32") {
|
|
671
|
-
return inspectWin32Executable(resolvedExecutablePath);
|
|
672
|
-
}
|
|
673
|
-
return undefined;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export async function inspectElectronAppPath(
|
|
677
|
-
appPath: string,
|
|
678
|
-
platform: NodeJS.Platform | ElectronDiscoveryPlatform = process.platform,
|
|
679
|
-
): Promise<ElectronAppDiscovery | undefined> {
|
|
680
|
-
if (platform === "darwin" || appPath.endsWith(".app")) {
|
|
681
|
-
return inspectDarwinApp(appPath);
|
|
682
|
-
}
|
|
683
|
-
return inspectElectronExecutablePath(appPath, platform);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export async function discoverElectronApps(options: DiscoverElectronAppsOptions = {}): Promise<ElectronDiscoveryResult> {
|
|
687
|
-
const platform = options.platform ?? process.platform;
|
|
688
|
-
const query = options.query?.trim() || undefined;
|
|
689
|
-
const maxResults = normalizeMaxResults(options.maxResults);
|
|
690
|
-
const locations = resolveLocations(options.locations);
|
|
691
|
-
let discovered: { apps: ElectronAppDiscovery[]; skippedCount: number };
|
|
692
|
-
if (platform === "darwin") {
|
|
693
|
-
discovered = await discoverDarwinApps(locations);
|
|
694
|
-
} else if (platform === "linux") {
|
|
695
|
-
discovered = await discoverLinuxApps(locations);
|
|
696
|
-
} else {
|
|
697
|
-
return { apps: [], maxResults, omittedCount: 0, platform: "unsupported", query };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const filteredApps = sortApps(dedupeApps(discovered.apps.map(annotateElectronAppSensitivity).filter((app) => appMatchesQuery(app, query))));
|
|
701
|
-
const apps = filteredApps.slice(0, maxResults);
|
|
702
|
-
return {
|
|
703
|
-
apps,
|
|
704
|
-
maxResults,
|
|
705
|
-
omittedCount: Math.max(0, filteredApps.length - apps.length),
|
|
706
|
-
platform,
|
|
707
|
-
query,
|
|
708
|
-
skippedCount: discovered.skippedCount || undefined,
|
|
709
|
-
};
|
|
710
|
-
}
|