browser-commander 0.24.0 → 0.26.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.
- package/.jscpd-baseline.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +7 -4
- package/package.json +4 -1
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +610 -25
- package/src/browser/launcher.js +14 -0
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +18 -1
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-launch.js +42 -0
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/safari-support.js +98 -0
- package/src/browser/storage-state.js +28 -8
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +64 -19
- package/src/cli/args.js +2 -0
- package/src/cli/commands.js +2 -0
- package/src/cli/high-level-methods.js +2 -0
- package/src/cli/sessions.js +5 -0
- package/src/core/webdriver-page.js +62 -0
- package/src/exports.js +6 -0
- package/src/traces/recorder.js +5 -0
- package/tests/e2e/safari.e2e.test.js +104 -0
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/browser/safari.test.js +192 -0
- package/tests/unit/browser/webdriver.test.js +48 -3
- package/tests/unit/cli/dispatcher.test.js +5 -1
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
package/.jscpd-baseline.json
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"0caa402c4eae32d8": 1,
|
|
21
21
|
"0fd23be8410c2c95": 1,
|
|
22
22
|
"0fe4f3b5e5e939b3": 1,
|
|
23
|
-
"119f429fe4d23e4a": 1,
|
|
24
23
|
"126a1cfe8f2264ce": 1,
|
|
25
24
|
"1273365073edd790": 1,
|
|
26
25
|
"13c4b58d3ea0719b": 1,
|
|
@@ -210,6 +209,7 @@
|
|
|
210
209
|
"c225b9d7d519303e": 1,
|
|
211
210
|
"c2c155adbcb46f3c": 1,
|
|
212
211
|
"c30737d62069fc02": 1,
|
|
212
|
+
"c32010db539b4efb": 1,
|
|
213
213
|
"c3c3569357673af8": 1,
|
|
214
214
|
"c415ffcd004d6518": 1,
|
|
215
215
|
"c461a83b51eb7740": 1,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8f9ef1c: Add native Safari and Technology Preview W3C WebDriver control through real/common launchers and CLI, with isolated cookie seeding, setup diagnostics and typed unsupported feature errors.
|
|
8
|
+
|
|
9
|
+
Update the transitive shell-quote lock entry to fix GHSA-pqg4-j6r4-53mv and unblock npm audit.
|
|
10
|
+
|
|
11
|
+
## 0.25.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 4a79d62: Import Safari bookmarks, history and explicit Passwords CSV exports into Chromium profiles. Discover named Safari profiles and modern WebKit cookie stores. Derive desktop browser executable discovery and profile protection from the shared catalogue, fix Opera Local State lookup, and report Yandex Ya Passman encryption limitations. Enforce domain boundaries for migrated cookies, history and passwords, and validate migration paths and options before writing.
|
|
16
|
+
|
|
17
|
+
Resolve native Safe Storage credentials from the catalogue and provide service-specific macOS Keychain retry guidance. SQLite stores that cannot be backed up consistently return close-browser/retry guidance instead of copying live database files.
|
|
18
|
+
|
|
19
|
+
Match exact hosts and subdomains in cookie-source metadata and automatic source selection, without decoding cookie values or treating domain strings as SQL wildcard patterns.
|
|
20
|
+
|
|
21
|
+
Keep Safari profile-catalogue errors alongside readable profiles and cookie sources, including domain-filtered listings.
|
|
22
|
+
|
|
23
|
+
Filter associated Login Data notes, security records and origin statistics; re-encrypt retained notes with the target key. Reset copied sync state and report unknown metadata omitted during domain-filtered imports.
|
|
24
|
+
|
|
25
|
+
Recognize all 18 migration data classes with explicit unsupported reports. Add separate payment-card consent across native APIs, pre-launch migration, CLI and command streams, plus provider-specific passkey and certificate export limitations without accessing protected stores.
|
|
26
|
+
|
|
27
|
+
Translate Firefox history to Chromium with exact microsecond visit dates, domain filtering, immutable snapshots and per-visit malformed-data reports.
|
|
28
|
+
|
|
29
|
+
Normalize Firefox schema 16+ cookie expiry from milliseconds to seconds in both installed-profile reading and migration, preserving legacy schemas and session markers.
|
|
30
|
+
|
|
31
|
+
Preserve supported Yandex Login Data imports when an unsupported Ya Passman Data store is present, retaining separate encryption diagnostics.
|
|
32
|
+
|
|
33
|
+
Omit derived cluster labels, keywords, duplicate-visit records and unknown History metadata with named warnings during domain-filtered imports; preserve them during unfiltered imports.
|
|
34
|
+
|
|
3
35
|
## 0.24.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -462,7 +462,7 @@ const session = await launchRealBrowser({
|
|
|
462
462
|
browser: 'chrome', // chrome | edge | brave | chromium | firefox
|
|
463
463
|
profile: 'Default',
|
|
464
464
|
include: ['cookies', 'bookmarks', 'history', 'passwords', 'preferences'],
|
|
465
|
-
domains: ['npmjs.com', 'github.com'], // optional
|
|
465
|
+
domains: ['npmjs.com', 'github.com'], // optional cookies/history/passwords filter
|
|
466
466
|
},
|
|
467
467
|
});
|
|
468
468
|
console.log(session.migration.migrated, session.migration.skipped);
|
|
@@ -524,9 +524,12 @@ overrides and managed downloads need CDP and throw on this engine; see
|
|
|
524
524
|
|
|
525
525
|
### listBrowserProfiles(options)
|
|
526
526
|
|
|
527
|
-
Discover
|
|
527
|
+
Discover profiles for the browsers in the shared catalogue, including Safari.
|
|
528
528
|
Pass an optional `browser` to narrow discovery. Each result contains
|
|
529
|
-
`{ browser, name, displayName, path, isDefault }`.
|
|
529
|
+
`{ browser, name, displayName, path, isDefault }`. If Safari's profile catalogue
|
|
530
|
+
cannot be read, a `Profiles` entry carries `error` alongside readable profiles.
|
|
531
|
+
Protected-file errors retain Full Disk Access instructions; cookie-source
|
|
532
|
+
listings preserve the same error even when a domain filter is supplied.
|
|
530
533
|
|
|
531
534
|
### readBrowserCookies(options)
|
|
532
535
|
|
|
@@ -589,7 +592,7 @@ const report = await migrateProfile({
|
|
|
589
592
|
'preferences',
|
|
590
593
|
'extensions',
|
|
591
594
|
],
|
|
592
|
-
domains: ['github.com'], // optional
|
|
595
|
+
domains: ['github.com'], // optional cookies/history/passwords filter
|
|
593
596
|
});
|
|
594
597
|
```
|
|
595
598
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -81,9 +81,12 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"better-sqlite3": "^13.0.3",
|
|
84
|
+
"bplist-parser": "^0.5.0",
|
|
84
85
|
"command-stream": "^1.4.0",
|
|
86
|
+
"csv-parse": "^7.0.3",
|
|
85
87
|
"links-notation": "^0.22.0",
|
|
86
88
|
"log-lazy": "^1.0.4",
|
|
89
|
+
"plist": "^5.0.0",
|
|
87
90
|
"test-anywhere": "^0.9.1"
|
|
88
91
|
},
|
|
89
92
|
"devDependencies": {
|
|
@@ -10,13 +10,11 @@ import {
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
|
|
13
|
+
import { browserFamily } from '../browser-sources.js';
|
|
15
14
|
import {
|
|
16
15
|
browserProfileRoot,
|
|
17
16
|
normalizeCookieBrowser,
|
|
18
17
|
} from '../browser-profiles.js';
|
|
19
|
-
import { pathExists } from '../migration/fs-utils.js';
|
|
20
18
|
import { withDatabaseSnapshot } from '../migration/sqlite-snapshot.js';
|
|
21
19
|
import {
|
|
22
20
|
createTemporaryUserDataDir,
|
|
@@ -171,28 +169,6 @@ async function prepareTarget(to, sourceRoot, profile) {
|
|
|
171
169
|
return to;
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
/**
|
|
175
|
-
* Copy a database snapshot to its place in the target. The Online Backup API
|
|
176
|
-
* snapshot is a single self-contained file; the locked-file fallback is a copy
|
|
177
|
-
* with its `-wal`/`-journal` sidecars, which is folded into one file with
|
|
178
|
-
* another backup (of the temporary copy, never of the source).
|
|
179
|
-
*/
|
|
180
|
-
async function copyDatabase(snapshotPath, destination) {
|
|
181
|
-
const hasSidecar =
|
|
182
|
-
(await pathExists(`${snapshotPath}-wal`)) ||
|
|
183
|
-
(await pathExists(`${snapshotPath}-journal`));
|
|
184
|
-
if (!hasSidecar) {
|
|
185
|
-
await copyFile(snapshotPath, destination);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const database = new BetterSqlite3(snapshotPath, { fileMustExist: true });
|
|
189
|
-
try {
|
|
190
|
-
await database.backup(destination);
|
|
191
|
-
} finally {
|
|
192
|
-
database.close();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
172
|
/** Copy the directory tree below one entry, recording the outcome. */
|
|
197
173
|
class SnapshotCopier {
|
|
198
174
|
constructor(sourceRoot, target) {
|
|
@@ -213,7 +189,7 @@ class SnapshotCopier {
|
|
|
213
189
|
if (await isSqliteFile(source)) {
|
|
214
190
|
await withDatabaseSnapshot({
|
|
215
191
|
sourcePath: source,
|
|
216
|
-
read: (snapshotPath) =>
|
|
192
|
+
read: (snapshotPath) => copyFile(snapshotPath, destination),
|
|
217
193
|
});
|
|
218
194
|
this.copied.databases += 1;
|
|
219
195
|
} else {
|
|
@@ -311,9 +287,9 @@ export async function snapshotUserDataDir({
|
|
|
311
287
|
throw new TypeError('snapshotUserDataDir requires a browser');
|
|
312
288
|
}
|
|
313
289
|
const normalizedBrowser = normalizeCookieBrowser(browser);
|
|
314
|
-
if (normalizedBrowser
|
|
290
|
+
if (browserFamily(normalizedBrowser) !== 'chromium') {
|
|
315
291
|
throw new Error(
|
|
316
|
-
'A profile snapshot is only supported for Chromium-family browsers
|
|
292
|
+
'A profile snapshot is only supported for Chromium-family browsers; this launcher uses CDP'
|
|
317
293
|
);
|
|
318
294
|
}
|
|
319
295
|
assertProfileName(profile);
|
|
@@ -68,15 +68,22 @@ export async function readSafeStoragePassword({
|
|
|
68
68
|
throw new Error(`No Safe Storage identity is known for ${browser}`);
|
|
69
69
|
}
|
|
70
70
|
if (platform === 'darwin') {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
try {
|
|
72
|
+
const password = await runCommand(
|
|
73
|
+
'security',
|
|
74
|
+
['find-generic-password', '-w', '-s', identity.service],
|
|
75
|
+
environment
|
|
76
|
+
);
|
|
77
|
+
if (!password) {
|
|
78
|
+
throw new Error(`${identity.service} returned an empty password`);
|
|
79
|
+
}
|
|
80
|
+
return password;
|
|
81
|
+
} catch (cause) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Could not read ${identity.service} from macOS Keychain. Unlock the login Keychain and allow the app running Browser Commander to access this item, then retry with refresh=true.`,
|
|
84
|
+
{ cause }
|
|
85
|
+
);
|
|
78
86
|
}
|
|
79
|
-
return password;
|
|
80
87
|
}
|
|
81
88
|
if (platform === 'linux') {
|
|
82
89
|
return readLinuxSafeStoragePassword(browser, environment, runCommand);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { localStatePathForProfile } from './browser-profile-files.js';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
resolveSourceBrowser,
|
|
34
34
|
} from './browser-profiles.js';
|
|
35
35
|
import { resolveDefaultBrowser } from './default-browser.js';
|
|
36
|
+
import { matchesDomains } from './migration/domains.js';
|
|
36
37
|
import {
|
|
37
38
|
countSafariCookies,
|
|
38
39
|
parseSafariCookies,
|
|
@@ -77,10 +78,16 @@ function readDatabaseVersion(database) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
// Firefox schema 16 changed expiry from Unix seconds to milliseconds. Normalize
|
|
82
|
+
// in SQLite before mapping so both installed reading and migration use seconds.
|
|
83
|
+
export function readFirefoxRows(database, domainFilter) {
|
|
84
|
+
const version = Number(
|
|
85
|
+
database.prepare('PRAGMA user_version').get()?.user_version ?? 0
|
|
86
|
+
);
|
|
87
|
+
const expiry = version >= 16 ? 'expiry / 1000 AS expiry' : 'expiry';
|
|
81
88
|
return queryRows(
|
|
82
89
|
database,
|
|
83
|
-
`SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite
|
|
90
|
+
`SELECT name, value, host, path, ${expiry}, isSecure, isHttpOnly, sameSite
|
|
84
91
|
FROM moz_cookies
|
|
85
92
|
${domainFilter ? 'WHERE host LIKE ?' : ''}
|
|
86
93
|
ORDER BY host, name, path`,
|
|
@@ -168,10 +175,7 @@ function chromiumKeyForPrefix(context, prefix) {
|
|
|
168
175
|
},
|
|
169
176
|
create: () =>
|
|
170
177
|
context.readWindowsEncryptionKey({
|
|
171
|
-
localStatePath: path
|
|
172
|
-
path.dirname(context.profile.path),
|
|
173
|
-
'Local State'
|
|
174
|
-
),
|
|
178
|
+
localStatePath: localStatePathForProfile(context.profile.path),
|
|
175
179
|
environment: context.environment,
|
|
176
180
|
decryptDpapi: context.decryptWindowsDpapi,
|
|
177
181
|
}),
|
|
@@ -380,21 +384,22 @@ export function readBrowserCookies(options) {
|
|
|
380
384
|
function countCookiesByDomain(database, family, domains) {
|
|
381
385
|
const column = family === 'firefox' ? 'host' : 'host_key';
|
|
382
386
|
const table = family === 'firefox' ? 'moz_cookies' : 'cookies';
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
: `SELECT COUNT(*) AS n FROM ${table}`;
|
|
387
|
-
const statement = database.prepare(query);
|
|
388
|
-
const row = filter ? statement.get(`%${filter}%`) : statement.get();
|
|
389
|
-
return Number(row?.n ?? 0);
|
|
390
|
-
};
|
|
391
|
-
const total = countFor(null);
|
|
387
|
+
const total = Number(
|
|
388
|
+
database.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()?.n ?? 0
|
|
389
|
+
);
|
|
392
390
|
if (!Array.isArray(domains) || domains.length === 0) {
|
|
393
391
|
return { total, byDomain: null };
|
|
394
392
|
}
|
|
395
|
-
const byDomain =
|
|
396
|
-
|
|
397
|
-
|
|
393
|
+
const byDomain = Object.fromEntries(domains.map((domain) => [domain, 0]));
|
|
394
|
+
const hosts = database.prepare(
|
|
395
|
+
`SELECT ${column} AS host, COUNT(*) AS n FROM ${table} GROUP BY ${column}`
|
|
396
|
+
);
|
|
397
|
+
for (const { host, n } of hosts.all()) {
|
|
398
|
+
for (const domain of Object.keys(byDomain)) {
|
|
399
|
+
if (matchesDomains(host ?? '', [domain])) {
|
|
400
|
+
byDomain[domain] += Number(n);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
398
403
|
}
|
|
399
404
|
return { total, byDomain };
|
|
400
405
|
}
|
|
@@ -426,6 +431,16 @@ export async function listCookieSources({
|
|
|
426
431
|
});
|
|
427
432
|
const sources = [];
|
|
428
433
|
for (const profile of profiles) {
|
|
434
|
+
const source = {
|
|
435
|
+
browser: profile.browser,
|
|
436
|
+
profile: profile.name,
|
|
437
|
+
path: profile.path,
|
|
438
|
+
isDefault: profile.isDefault,
|
|
439
|
+
};
|
|
440
|
+
if (profile.error) {
|
|
441
|
+
sources.push({ ...source, error: profile.error });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
429
444
|
const cookiePath = await findCookieDatabase(
|
|
430
445
|
profile.browser,
|
|
431
446
|
profile.path,
|
|
@@ -454,13 +469,7 @@ export async function listCookieSources({
|
|
|
454
469
|
database?.close();
|
|
455
470
|
}
|
|
456
471
|
if (error) {
|
|
457
|
-
sources.push({
|
|
458
|
-
browser: profile.browser,
|
|
459
|
-
profile: profile.name,
|
|
460
|
-
path: profile.path,
|
|
461
|
-
isDefault: profile.isDefault,
|
|
462
|
-
error,
|
|
463
|
-
});
|
|
472
|
+
sources.push({ ...source, error });
|
|
464
473
|
continue;
|
|
465
474
|
}
|
|
466
475
|
// When filtering by domain, skip profiles that hold none of them.
|
|
@@ -471,10 +480,7 @@ export async function listCookieSources({
|
|
|
471
480
|
continue;
|
|
472
481
|
}
|
|
473
482
|
sources.push({
|
|
474
|
-
|
|
475
|
-
profile: profile.name,
|
|
476
|
-
path: profile.path,
|
|
477
|
-
isDefault: profile.isDefault,
|
|
483
|
+
...source,
|
|
478
484
|
cookies: counts.total,
|
|
479
485
|
byDomain: counts.byDomain,
|
|
480
486
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Opera keeps Local State inside its single-profile root. */
|
|
5
|
+
export function localStatePathForProfile(profileDir) {
|
|
6
|
+
const own = path.join(profileDir, 'Local State');
|
|
7
|
+
return existsSync(own)
|
|
8
|
+
? own
|
|
9
|
+
: path.join(path.dirname(profileDir), 'Local State');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Resolve existing ancestors even when the target profile does not exist. */
|
|
13
|
+
export function physicalPath(value) {
|
|
14
|
+
let existing = path.resolve(value);
|
|
15
|
+
const suffix = [];
|
|
16
|
+
while (true) {
|
|
17
|
+
try {
|
|
18
|
+
return path.join(realpathSync(existing), ...suffix.reverse());
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
suffix.push(path.basename(existing));
|
|
24
|
+
const parent = path.dirname(existing);
|
|
25
|
+
if (parent === existing) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
existing = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './browser-sources.js';
|
|
12
12
|
import { resolveDefaultBrowser } from './default-browser.js';
|
|
13
13
|
import { findSafariCookieFile } from './safari-cookies.js';
|
|
14
|
+
import { listSafariProfiles } from './safari-profiles.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Keywords that select the operating-system default browser instead of a named
|
|
@@ -279,16 +280,9 @@ async function listProfilesForBrowser(browser, platform, homeDir, environment) {
|
|
|
279
280
|
const profiles = [];
|
|
280
281
|
for (const root of roots) {
|
|
281
282
|
if (family === 'safari') {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
browser,
|
|
286
|
-
name: 'Default',
|
|
287
|
-
displayName: 'Default',
|
|
288
|
-
path: root,
|
|
289
|
-
isDefault: true,
|
|
290
|
-
},
|
|
291
|
-
];
|
|
283
|
+
const safariProfiles = await listSafariProfiles(browser, root);
|
|
284
|
+
if (safariProfiles.length) {
|
|
285
|
+
return safariProfiles;
|
|
292
286
|
}
|
|
293
287
|
continue;
|
|
294
288
|
}
|
|
@@ -301,7 +295,7 @@ async function listProfilesForBrowser(browser, platform, homeDir, environment) {
|
|
|
301
295
|
return profiles;
|
|
302
296
|
}
|
|
303
297
|
|
|
304
|
-
/** Discover
|
|
298
|
+
/** Discover profiles, retaining per-source errors alongside readable profiles. */
|
|
305
299
|
export async function listBrowserProfiles({
|
|
306
300
|
browser,
|
|
307
301
|
platform = process.platform,
|
|
@@ -330,10 +324,11 @@ export async function listBrowserProfiles({
|
|
|
330
324
|
homeDir,
|
|
331
325
|
environment
|
|
332
326
|
)) {
|
|
333
|
-
|
|
327
|
+
const identity = JSON.stringify([profile.path, Boolean(profile.error)]);
|
|
328
|
+
if (seen.has(identity)) {
|
|
334
329
|
continue;
|
|
335
330
|
}
|
|
336
|
-
seen.add(
|
|
331
|
+
seen.add(identity);
|
|
337
332
|
profiles.push(profile);
|
|
338
333
|
}
|
|
339
334
|
}
|
|
@@ -360,5 +355,8 @@ export async function resolveBrowserProfile(options) {
|
|
|
360
355
|
const detail = requested ? ` profile "${requested}"` : ' profile';
|
|
361
356
|
throw new Error(`Could not find a cookie database for ${browser}${detail}`);
|
|
362
357
|
}
|
|
358
|
+
if (selected.error) {
|
|
359
|
+
throw new Error(selected.error);
|
|
360
|
+
}
|
|
363
361
|
return selected;
|
|
364
362
|
}
|
|
@@ -100,6 +100,8 @@ function templateVariables(platform, homeDir, environment) {
|
|
|
100
100
|
environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local'),
|
|
101
101
|
appData:
|
|
102
102
|
environment.APPDATA ?? pathApi.join(homeDir, 'AppData', 'Roaming'),
|
|
103
|
+
programFiles: environment.PROGRAMFILES,
|
|
104
|
+
programFilesX86: environment['PROGRAMFILES(X86)'],
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
return {
|
|
@@ -133,7 +135,29 @@ function expandTemplate(template, variables, pathApi) {
|
|
|
133
135
|
* @param {Object} [options.environment=process.env]
|
|
134
136
|
* @returns {string[]}
|
|
135
137
|
*/
|
|
136
|
-
export function resolveBrowserRoots(
|
|
138
|
+
export function resolveBrowserRoots(name, options = {}) {
|
|
139
|
+
const source = normalizeBrowserSource(name);
|
|
140
|
+
const templates = source.roots?.[options.platform ?? process.platform] ?? [];
|
|
141
|
+
return resolveTemplates(templates, options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveTemplates(
|
|
145
|
+
templates,
|
|
146
|
+
{
|
|
147
|
+
platform = process.platform,
|
|
148
|
+
homeDir = os.homedir(),
|
|
149
|
+
environment = process.env,
|
|
150
|
+
} = {}
|
|
151
|
+
) {
|
|
152
|
+
const pathApi = platformPath(platform);
|
|
153
|
+
const variables = templateVariables(platform, homeDir, environment);
|
|
154
|
+
return templates
|
|
155
|
+
.map((template) => expandTemplate(template, variables, pathApi))
|
|
156
|
+
.filter((entry) => entry !== undefined);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Installed executable candidates, from the same catalogue as profile roots. */
|
|
160
|
+
export function resolveBrowserExecutables(
|
|
137
161
|
name,
|
|
138
162
|
{
|
|
139
163
|
platform = process.platform,
|
|
@@ -142,17 +166,23 @@ export function resolveBrowserRoots(
|
|
|
142
166
|
} = {}
|
|
143
167
|
) {
|
|
144
168
|
const source = normalizeBrowserSource(name);
|
|
145
|
-
const templates = source.roots?.[platform] ?? [];
|
|
146
169
|
const pathApi = platformPath(platform);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
const candidates = resolveTemplates(source.executables?.[platform] ?? [], {
|
|
171
|
+
platform,
|
|
172
|
+
homeDir,
|
|
173
|
+
environment,
|
|
174
|
+
});
|
|
175
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter;
|
|
176
|
+
for (const directory of (environment.PATH ?? '')
|
|
177
|
+
.split(delimiter)
|
|
178
|
+
.filter(Boolean)) {
|
|
179
|
+
for (const name of source.executableNames ?? []) {
|
|
180
|
+
candidates.push(
|
|
181
|
+
pathApi.join(directory, platform === 'win32' ? `${name}.exe` : name)
|
|
182
|
+
);
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
|
-
return
|
|
185
|
+
return [...new Set(candidates)];
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
/** True when a browser stores one profile in the root itself (Opera-style). */
|