package-registry-manager 0.22.2 → 0.22.4
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/package.json +10 -8
- package/src/automation.mjs +21 -11
- package/src/browser-catalogue.mjs +209 -0
- package/src/browser-options.mjs +39 -14
- package/src/cli.mjs +13 -8
- package/src/default-browser.mjs +35 -19
- package/src/profile.mjs +10 -3
- package/src/sign-in-import.mjs +14 -26
- package/src/webdriver-automation.mjs +125 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "package-registry-manager",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Inspect repositories and guide secure package-registry setup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": "./src/index.mjs",
|
|
11
11
|
"engines": {
|
|
12
|
-
"node": ">=22"
|
|
12
|
+
"node": ">=22.12.0"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"provenance": true
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"syntax": "node --check src/cli.mjs && node --check src/direct-execution.mjs && node --check src/index.mjs && node --check src/browser.mjs && node --check src/browser-options.mjs && node --check src/automation.mjs && node --check src/profile.mjs && node --check src/setup.mjs && node --check src/auth-urls.mjs && node --check src/containers.mjs && node --check src/flows.mjs && node --check src/prerequisites.mjs && node --check src/registry-state.mjs && node --check src/workflows.mjs && node --check src/publishers.mjs && node --check src/skips.mjs && node --check src/tokens.mjs && node --check src/approvals.mjs && node --check src/default-browser.mjs && node --check src/pages.mjs && node --check src/crates-api.mjs && node --check src/sign-in-import.mjs && node --check scripts/publish-to-npm.mjs && node --check scripts/setup-npm.mjs && node --check scripts/sanitize-npm-userconfig.mjs",
|
|
31
|
+
"syntax": "node --check src/cli.mjs && node --check src/direct-execution.mjs && node --check src/index.mjs && node --check src/browser.mjs && node --check src/browser-options.mjs && node --check src/browser-catalogue.mjs && node --check src/automation.mjs && node --check src/webdriver-automation.mjs && node --check src/profile.mjs && node --check src/setup.mjs && node --check src/auth-urls.mjs && node --check src/containers.mjs && node --check src/flows.mjs && node --check src/prerequisites.mjs && node --check src/registry-state.mjs && node --check src/workflows.mjs && node --check src/publishers.mjs && node --check src/skips.mjs && node --check src/tokens.mjs && node --check src/approvals.mjs && node --check src/default-browser.mjs && node --check src/pages.mjs && node --check src/crates-api.mjs && node --check src/sign-in-import.mjs && node --check scripts/publish-to-npm.mjs && node --check scripts/setup-npm.mjs && node --check scripts/sanitize-npm-userconfig.mjs",
|
|
32
32
|
"lint": "eslint src test scripts --max-warnings 0",
|
|
33
33
|
"lint:fix": "eslint src test scripts --fix --max-warnings 0",
|
|
34
34
|
"format": "prettier --write .",
|
|
@@ -36,16 +36,18 @@
|
|
|
36
36
|
"check:duplication": "jscpd src",
|
|
37
37
|
"check": "npm run syntax && npm run lint && npm run format:check && npm run check:duplication",
|
|
38
38
|
"test": "node --test --test-timeout=30000",
|
|
39
|
-
"pack:check": "npm pack --dry-run"
|
|
39
|
+
"pack:check": "npm pack --dry-run",
|
|
40
|
+
"audit": "node scripts/npm-audit.mjs"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"browser-commander": "^0.
|
|
43
|
-
"command-stream": "^1.
|
|
44
|
-
"playwright": "^1.63.0"
|
|
43
|
+
"browser-commander": "^0.25.0",
|
|
44
|
+
"command-stream": "^1.6.2",
|
|
45
|
+
"playwright": "^1.63.0",
|
|
46
|
+
"selenium-webdriver": "^4.50.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@eslint/js": "^10.0.1",
|
|
48
|
-
"eslint": "^10.
|
|
50
|
+
"eslint": "^10.12.0",
|
|
49
51
|
"eslint-config-prettier": "^10.1.8",
|
|
50
52
|
"jscpd": "^5.4.0",
|
|
51
53
|
"prettier": "^3.9.9"
|
package/src/automation.mjs
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
attachViaExtension,
|
|
6
6
|
EXTENSION_DIRECTORY,
|
|
7
7
|
launchRealBrowser,
|
|
8
|
+
findBrowserSource,
|
|
8
9
|
} from "browser-commander";
|
|
9
10
|
|
|
11
|
+
import { connectWebDriverAutomation } from "./webdriver-automation.mjs";
|
|
12
|
+
|
|
10
13
|
import { snapshotBrowser } from "./browser-options.mjs";
|
|
11
14
|
import { defaultBrowserProfile, ensureProfileIgnored } from "./profile.mjs";
|
|
12
15
|
import { IMPORT_CHOICES, migrationFor } from "./sign-in-import.mjs";
|
|
@@ -28,7 +31,10 @@ export function relayExtensionDirectory() {
|
|
|
28
31
|
*/
|
|
29
32
|
export function launchOptions(browser, profile, verbose = false, domains = []) {
|
|
30
33
|
const options = {
|
|
31
|
-
engine:
|
|
34
|
+
engine:
|
|
35
|
+
findBrowserSource(browser.channel)?.controlProtocol === "cdp"
|
|
36
|
+
? "playwright"
|
|
37
|
+
: "selenium",
|
|
32
38
|
channel: browser.channel,
|
|
33
39
|
headless: false,
|
|
34
40
|
verbose,
|
|
@@ -75,25 +81,29 @@ export function extensionInstructions(directory) {
|
|
|
75
81
|
* Connects the browser that fills forms and returns a page with `goto`,
|
|
76
82
|
* `evaluate`, `close`, and, for a launched profile, `clearCookies(domains)`.
|
|
77
83
|
*/
|
|
78
|
-
export async function connectAutomation(
|
|
79
|
-
browser,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
domains = [],
|
|
83
|
-
}) {
|
|
84
|
+
export async function connectAutomation(
|
|
85
|
+
{ browser, profile, verbose = false, domains = [] },
|
|
86
|
+
dependencies = {},
|
|
87
|
+
) {
|
|
84
88
|
if (browser.attach?.mode === "extension") {
|
|
85
89
|
return connectExtension();
|
|
86
90
|
}
|
|
91
|
+
if (findBrowserSource(browser.channel)?.controlProtocol === "bidi") {
|
|
92
|
+
return connectWebDriverAutomation(
|
|
93
|
+
{ browser, profile, verbose, domains },
|
|
94
|
+
dependencies,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
87
97
|
if (!browser.attach) {
|
|
88
98
|
await ensureProfileIgnored(profile, { verbose });
|
|
89
99
|
}
|
|
90
|
-
const connection = await
|
|
91
|
-
|
|
92
|
-
);
|
|
100
|
+
const connection = await (
|
|
101
|
+
dependencies.launchRealBrowser ?? launchRealBrowser
|
|
102
|
+
)(launchOptions(browser, profile, verbose, domains));
|
|
93
103
|
return {
|
|
94
104
|
goto: (url) => connection.page.goto(url),
|
|
95
105
|
evaluate: (script) => connection.page.evaluate(script),
|
|
96
|
-
close: () => connection.
|
|
106
|
+
close: () => connection.close(),
|
|
97
107
|
async clearCookies(names) {
|
|
98
108
|
for (const name of names) {
|
|
99
109
|
const escaped = name.replaceAll(".", "\\.");
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { constants, existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { BROWSER_IDS, findBrowserSource } from "browser-commander";
|
|
7
|
+
|
|
8
|
+
/** Catalogue entries reached through Browser Commander's public exports. */
|
|
9
|
+
export const browserSources = () => BROWSER_IDS.map(findBrowserSource);
|
|
10
|
+
|
|
11
|
+
/** Catalogue channels with a launch control protocol. */
|
|
12
|
+
export function launchChannels() {
|
|
13
|
+
return browserSources()
|
|
14
|
+
.filter((source) => source.controlProtocol)
|
|
15
|
+
.flatMap((source) => [source.id, ...(source.aliases ?? [])]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A readable name from the catalogue's application path or canonical alias. */
|
|
19
|
+
export function catalogueName(id) {
|
|
20
|
+
const source = findBrowserSource(id);
|
|
21
|
+
if (!source) {
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
const app = source.executables?.darwin?.[0]?.match(/\/([^/]+)\.app\//)?.[1];
|
|
25
|
+
// Preserve the established short name of Brave.
|
|
26
|
+
if (app) {
|
|
27
|
+
return app === "Brave Browser" ? "Brave" : app;
|
|
28
|
+
}
|
|
29
|
+
const label = [source.id, ...(source.aliases ?? [])]
|
|
30
|
+
.map((name) => name.replace(/^apple-/, ""))
|
|
31
|
+
.reduce((longest, name) => (name.length > longest.length ? name : longest));
|
|
32
|
+
return label
|
|
33
|
+
.replace(/^apple-/, "")
|
|
34
|
+
.split("-")
|
|
35
|
+
.map((word) =>
|
|
36
|
+
word.length <= 2
|
|
37
|
+
? word.toUpperCase()
|
|
38
|
+
: word[0].toUpperCase() + word.slice(1),
|
|
39
|
+
)
|
|
40
|
+
.join(" ")
|
|
41
|
+
.replace("Duckduckgo", "DuckDuckGo");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Installed executables or existing profile roots; never reads cookie data. */
|
|
45
|
+
export function installedBrowsers(options = {}) {
|
|
46
|
+
const exists = options.exists ?? existsSync;
|
|
47
|
+
const platform = options.platform ?? process.platform;
|
|
48
|
+
const environment = options.environment ?? process.env;
|
|
49
|
+
const discovery = {
|
|
50
|
+
...options,
|
|
51
|
+
platform,
|
|
52
|
+
environment:
|
|
53
|
+
platform === "win32"
|
|
54
|
+
? Object.fromEntries(
|
|
55
|
+
Object.entries(environment).map(([key, value]) => [
|
|
56
|
+
key.toUpperCase(),
|
|
57
|
+
value,
|
|
58
|
+
]),
|
|
59
|
+
)
|
|
60
|
+
: environment,
|
|
61
|
+
};
|
|
62
|
+
return browserSources()
|
|
63
|
+
.filter((source) =>
|
|
64
|
+
installedCandidates(source, discovery).some((candidate) =>
|
|
65
|
+
exists(candidate),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
.map((source) => source.id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function installedCandidates(
|
|
72
|
+
source,
|
|
73
|
+
{
|
|
74
|
+
platform = process.platform,
|
|
75
|
+
homeDir = os.homedir(),
|
|
76
|
+
environment = process.env,
|
|
77
|
+
},
|
|
78
|
+
) {
|
|
79
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
80
|
+
const variables = {
|
|
81
|
+
home: homeDir,
|
|
82
|
+
config: environment.XDG_CONFIG_HOME ?? pathApi.join(homeDir, ".config"),
|
|
83
|
+
appSupport: pathApi.join(homeDir, "Library", "Application Support"),
|
|
84
|
+
appData: environment.APPDATA ?? pathApi.join(homeDir, "AppData", "Roaming"),
|
|
85
|
+
localAppData:
|
|
86
|
+
environment.LOCALAPPDATA ?? pathApi.join(homeDir, "AppData", "Local"),
|
|
87
|
+
programFiles: environment.PROGRAMFILES,
|
|
88
|
+
programFilesX86: environment["PROGRAMFILES(X86)"],
|
|
89
|
+
};
|
|
90
|
+
const templates = [
|
|
91
|
+
...(source.executables?.[platform] ?? []),
|
|
92
|
+
...(source.roots?.[platform] ?? []),
|
|
93
|
+
];
|
|
94
|
+
const candidates = templates.flatMap((template) => {
|
|
95
|
+
const match = /^\{(\w+)\}\/?(.*)$/.exec(template);
|
|
96
|
+
if (!match) {
|
|
97
|
+
return [template];
|
|
98
|
+
}
|
|
99
|
+
const base = variables[match[1]];
|
|
100
|
+
return base === undefined
|
|
101
|
+
? []
|
|
102
|
+
: [pathApi.join(base, ...match[2].split("/"))];
|
|
103
|
+
});
|
|
104
|
+
for (const directory of (environment.PATH ?? "")
|
|
105
|
+
.split(platform === "win32" ? ";" : ":")
|
|
106
|
+
.filter(Boolean)) {
|
|
107
|
+
for (const name of source.executableNames ?? []) {
|
|
108
|
+
candidates.push(
|
|
109
|
+
pathApi.join(directory, platform === "win32" ? `${name}.exe` : name),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return candidates;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Discovery information included when an import or channel is invalid. */
|
|
117
|
+
export function installedDescription(options = {}) {
|
|
118
|
+
const found = installedBrowsers(options);
|
|
119
|
+
return `Installed browsers found: ${found.join(", ") || "none"}.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Reject unavailable engines before creating an automated profile. */
|
|
123
|
+
export function validateChannel(channel) {
|
|
124
|
+
const known = findBrowserSource(channel);
|
|
125
|
+
if (known?.controlProtocol) {
|
|
126
|
+
return channel;
|
|
127
|
+
}
|
|
128
|
+
const reason = known
|
|
129
|
+
? `${channel} has no launch control protocol in browser-commander's catalogue${known.family === "safari" ? " (Safari setup: https://github.com/link-foundation/browser-commander/issues/126)" : ""}`
|
|
130
|
+
: `unknown --browser-channel '${channel}'`;
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${reason}; choose from ${launchChannels().join(", ")}. ${installedDescription()}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resolves the selected installed binary using catalogue paths and PATH. */
|
|
137
|
+
export async function resolveExecutable(
|
|
138
|
+
{ channel, executablePath },
|
|
139
|
+
discovery = {},
|
|
140
|
+
) {
|
|
141
|
+
const platform = discovery.platform ?? process.platform;
|
|
142
|
+
const environment = discovery.environment ?? process.env;
|
|
143
|
+
const options = {
|
|
144
|
+
...discovery,
|
|
145
|
+
platform,
|
|
146
|
+
environment:
|
|
147
|
+
platform === "win32"
|
|
148
|
+
? Object.fromEntries(
|
|
149
|
+
Object.entries(environment).map(([key, value]) => [
|
|
150
|
+
key.toUpperCase(),
|
|
151
|
+
value,
|
|
152
|
+
]),
|
|
153
|
+
)
|
|
154
|
+
: environment,
|
|
155
|
+
};
|
|
156
|
+
const source = findBrowserSource(channel);
|
|
157
|
+
const candidates = executablePath
|
|
158
|
+
? [executablePath]
|
|
159
|
+
: installedCandidates({ ...source, roots: {} }, options);
|
|
160
|
+
for (const candidate of candidates) {
|
|
161
|
+
try {
|
|
162
|
+
await (discovery.access ?? access)(candidate, constants.X_OK);
|
|
163
|
+
return candidate;
|
|
164
|
+
} catch {
|
|
165
|
+
// Continue through the catalogue's executable paths and PATH names.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw new Error(
|
|
169
|
+
`could not find an installed ${channel} browser; use --browser-executable`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Refuses a source profile even when reached through a symlink. */
|
|
174
|
+
export function assertDedicatedProfile(profile, discovery = {}) {
|
|
175
|
+
const physical = (file) => {
|
|
176
|
+
try {
|
|
177
|
+
return realpathSync(file);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error.code !== "ENOENT") {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
return path.join(physical(path.dirname(file)), path.basename(file));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const requested = physical(path.resolve(profile));
|
|
186
|
+
const roots = browserSources().flatMap((source) =>
|
|
187
|
+
installedCandidates(
|
|
188
|
+
{
|
|
189
|
+
...source,
|
|
190
|
+
executables: {},
|
|
191
|
+
executableNames: [],
|
|
192
|
+
},
|
|
193
|
+
discovery,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
for (const root of roots) {
|
|
197
|
+
const relative = path.relative(physical(path.resolve(root)), requested);
|
|
198
|
+
if (
|
|
199
|
+
relative === "" ||
|
|
200
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
201
|
+
relative !== ".." &&
|
|
202
|
+
!path.isAbsolute(relative))
|
|
203
|
+
) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"automation requires a dedicated profile, not a browser default profile; use --browser-import to reuse sign-in",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
package/src/browser-options.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
findBrowserSource,
|
|
4
5
|
LAUNCH_RESTRICTION_PRESETS,
|
|
5
6
|
LAUNCH_RESTRICTIONS,
|
|
6
7
|
} from "browser-commander";
|
|
7
8
|
|
|
9
|
+
import { installedDescription, validateChannel } from "./browser-catalogue.mjs";
|
|
8
10
|
import {
|
|
9
11
|
IMPORT_CHOICES,
|
|
10
12
|
IMPORT_SCOPES,
|
|
@@ -50,10 +52,14 @@ export function parseBrowserOptions({
|
|
|
50
52
|
profileGiven = false,
|
|
51
53
|
} = {}) {
|
|
52
54
|
const options = {
|
|
53
|
-
channel,
|
|
55
|
+
channel: attach === "extension" ? channel : validateChannel(channel),
|
|
54
56
|
executable: executable ? path.resolve(executable) : null,
|
|
55
57
|
import: importFrom ? parseImport(importFrom) : null,
|
|
56
|
-
importScope:
|
|
58
|
+
importScope:
|
|
59
|
+
(importScope === undefined || importScope === null) &&
|
|
60
|
+
findBrowserSource(channel)?.controlProtocol === "bidi"
|
|
61
|
+
? "domains"
|
|
62
|
+
: parseImportScope(importScope, importFrom),
|
|
57
63
|
attach: attach ? parseAttach(attach) : null,
|
|
58
64
|
preferences: parsePreferences(preferences),
|
|
59
65
|
restrictions: validateRestrictions(restrictions),
|
|
@@ -78,25 +84,47 @@ export function parseBrowserOptions({
|
|
|
78
84
|
"--browser-attach extension cannot be combined with --browser-executable, --browser-pref, or --browser-restriction",
|
|
79
85
|
);
|
|
80
86
|
}
|
|
87
|
+
validateEngineOptions(options);
|
|
81
88
|
return options;
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
/** Rejects capabilities the installed upstream non-CDP launchers lack. */
|
|
92
|
+
function validateEngineOptions(options) {
|
|
93
|
+
if (
|
|
94
|
+
options.attach?.mode === "extension" ||
|
|
95
|
+
findBrowserSource(options.channel)?.controlProtocol === "cdp"
|
|
96
|
+
) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const unsupported = options.attach
|
|
100
|
+
? "--browser-attach snapshot"
|
|
101
|
+
: options.restrictions.length
|
|
102
|
+
? "--browser-restriction"
|
|
103
|
+
: Object.keys(options.preferences).length
|
|
104
|
+
? "--browser-pref"
|
|
105
|
+
: options.import && options.importScope === "full"
|
|
106
|
+
? "--browser-import-scope full"
|
|
107
|
+
: null;
|
|
108
|
+
if (unsupported) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`${unsupported} is unavailable for ${options.channel} in the installed browser-commander; use --browser-import-scope domains for sign-in cookies`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
/** Parses `<browser>[:profile]`, `default`, or `auto`. */
|
|
85
|
-
export function parseImport(spec) {
|
|
116
|
+
export function parseImport(spec, discovery = {}) {
|
|
86
117
|
const separator = spec.indexOf(":");
|
|
87
118
|
const browser = separator === -1 ? spec : spec.slice(0, separator);
|
|
88
119
|
const profile = separator === -1 ? null : spec.slice(separator + 1);
|
|
89
120
|
const choice = IMPORT_CHOICES.includes(browser);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
profile === "" ||
|
|
93
|
-
(choice && profile !== null)
|
|
94
|
-
) {
|
|
121
|
+
const source = findBrowserSource(browser);
|
|
122
|
+
if ((!source && !choice) || profile === "" || (choice && profile !== null)) {
|
|
95
123
|
throw new Error(
|
|
96
|
-
`--browser-import must be <${importSources().join("|")}>[:profile], default, or auto`,
|
|
124
|
+
`--browser-import must be <${importSources().join("|")}>[:profile], default, or auto. ${installedDescription(discovery)}`,
|
|
97
125
|
);
|
|
98
126
|
}
|
|
99
|
-
return { browser, profile };
|
|
127
|
+
return { browser: source?.id ?? browser, profile };
|
|
100
128
|
}
|
|
101
129
|
|
|
102
130
|
/**
|
|
@@ -184,10 +212,7 @@ export function validateRestrictions(names) {
|
|
|
184
212
|
|
|
185
213
|
/** The installed browser a snapshot copies for a launch channel. */
|
|
186
214
|
export function snapshotBrowser(channel) {
|
|
187
|
-
|
|
188
|
-
return "edge";
|
|
189
|
-
}
|
|
190
|
-
return ["brave", "chromium"].includes(channel) ? channel : "chrome";
|
|
215
|
+
return findBrowserSource(channel)?.id ?? channel;
|
|
191
216
|
}
|
|
192
217
|
|
|
193
218
|
/** Describes where forms are filled, for the browser prerequisite. */
|
package/src/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
|
|
9
|
+
import { launchChannels } from "./browser-catalogue.mjs";
|
|
9
10
|
import { parseBrowserOptions } from "./browser-options.mjs";
|
|
10
11
|
import { isDirectExecution } from "./direct-execution.mjs";
|
|
11
12
|
import { inspectRepository } from "./discovery.mjs";
|
|
@@ -14,6 +15,7 @@ import { buildPlans } from "./plan.mjs";
|
|
|
14
15
|
import { probeEnvironment, renderPrerequisites } from "./prerequisites.mjs";
|
|
15
16
|
import { probeRegistryState } from "./registry-state.mjs";
|
|
16
17
|
import { BROWSER_MODES, defaultBrowserProfile, executePlan } from "./setup.mjs";
|
|
18
|
+
import { importSources } from "./sign-in-import.mjs";
|
|
17
19
|
|
|
18
20
|
export { isDirectExecution };
|
|
19
21
|
|
|
@@ -62,17 +64,16 @@ Setup options:
|
|
|
62
64
|
--browser <default|automated> Open sign-in and approval pages in your
|
|
63
65
|
default browser, or in the automated
|
|
64
66
|
profile too (default: default)
|
|
65
|
-
--browser-channel <channel> Installed browser channel: chrome
|
|
66
|
-
|
|
67
|
-
msedge-canary (default: chrome)
|
|
67
|
+
--browser-channel <channel> Installed browser channel (default: chrome):
|
|
68
|
+
${launchChannels().join(", ")}
|
|
68
69
|
--browser-executable <path> Installed browser executable to launch
|
|
69
70
|
instead of the channel's
|
|
70
71
|
--browser-profile <path> Dedicated automation profile, used to fill
|
|
71
72
|
forms (default: per-user state directory)
|
|
72
73
|
--browser-import <browser>[:<profile>]|default|auto
|
|
73
|
-
Copy data from your
|
|
74
|
-
|
|
75
|
-
automated profile
|
|
74
|
+
Copy data from your installed browser profile:
|
|
75
|
+
${importSources().join(", ")}
|
|
76
|
+
into the automated profile; default takes the
|
|
76
77
|
system default browser, auto the first
|
|
77
78
|
browser signed in to the registry
|
|
78
79
|
--browser-import-scope <full|domains>
|
|
@@ -226,7 +227,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
226
227
|
browser: values.browser,
|
|
227
228
|
browserOptions,
|
|
228
229
|
browserProfile: path.resolve(
|
|
229
|
-
values["browser-profile"] ??
|
|
230
|
+
values["browser-profile"] ??
|
|
231
|
+
defaultBrowserProfile({ channel: browserOptions.channel }),
|
|
230
232
|
),
|
|
231
233
|
verbose: values.verbose,
|
|
232
234
|
verifyRelease: values["verify-release"],
|
|
@@ -272,7 +274,10 @@ function browserSummary(command, values, browserOptions) {
|
|
|
272
274
|
channel: browserOptions.channel,
|
|
273
275
|
profile:
|
|
274
276
|
values.browser === "automated" && !browserOptions.attach
|
|
275
|
-
? path.resolve(
|
|
277
|
+
? path.resolve(
|
|
278
|
+
values["browser-profile"] ??
|
|
279
|
+
defaultBrowserProfile({ channel: browserOptions.channel }),
|
|
280
|
+
)
|
|
276
281
|
: undefined,
|
|
277
282
|
import: browserOptions.import ?? undefined,
|
|
278
283
|
attach: browserOptions.attach ?? undefined,
|
package/src/default-browser.mjs
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { exec } from "command-stream";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Names of common browsers by macOS bundle id, Linux desktop entry, and
|
|
5
|
-
* Windows URL ProgId, so the tool can say which browser it opened.
|
|
6
|
-
*/
|
|
7
|
-
const KNOWN_BROWSERS = [
|
|
8
|
-
[/^com\.apple\.safari$|^SafariURL/i, "Safari"],
|
|
9
|
-
[/^com\.google\.chrome\.canary$/i, "Google Chrome Canary"],
|
|
10
|
-
[/^com\.google\.chrome$|google-chrome|^ChromeHTML$/i, "Google Chrome"],
|
|
11
|
-
[/^org\.chromium\.chromium$|chromium|^ChromiumHTM/i, "Chromium"],
|
|
12
|
-
[/^org\.mozilla\.firefox$|firefox|^FirefoxURL/i, "Firefox"],
|
|
13
|
-
[/^com\.microsoft\.edgemac$|microsoft-edge|^MSEdgeHTM$/i, "Microsoft Edge"],
|
|
14
|
-
[/^com\.brave\.browser$|brave-browser|^BraveHTML$/i, "Brave"],
|
|
15
|
-
[/^company\.thebrowser\.browser$/i, "Arc"],
|
|
16
|
-
[/^com\.operasoftware\.opera$|opera|^OperaStable$/i, "Opera"],
|
|
17
|
-
[/^com\.vivaldi\.vivaldi$|vivaldi|^VivaldiHTM/i, "Vivaldi"],
|
|
18
|
-
[/^IE\.HTTP/i, "Internet Explorer"],
|
|
19
|
-
];
|
|
3
|
+
import { browserSources, catalogueName } from "./browser-catalogue.mjs";
|
|
20
4
|
|
|
21
5
|
/** The command that reports the default browser, as an exact argument vector. */
|
|
22
6
|
export function defaultBrowserQuery(platform = process.platform) {
|
|
@@ -100,8 +84,40 @@ export function browserName(id) {
|
|
|
100
84
|
if (!id) {
|
|
101
85
|
return undefined;
|
|
102
86
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
87
|
+
const needle = id.trim().toLowerCase();
|
|
88
|
+
const sources = browserSources();
|
|
89
|
+
const known =
|
|
90
|
+
sources.find((source) =>
|
|
91
|
+
Object.values(source.default ?? {})
|
|
92
|
+
.flat()
|
|
93
|
+
.some((candidate) => needle === candidate.toLowerCase()),
|
|
94
|
+
) ??
|
|
95
|
+
sources.find(
|
|
96
|
+
(source) =>
|
|
97
|
+
(source.default?.win32 ?? []).some((candidate) =>
|
|
98
|
+
needle.startsWith(candidate.toLowerCase()),
|
|
99
|
+
) ||
|
|
100
|
+
(needle.endsWith(".desktop") &&
|
|
101
|
+
(source.executableNames ?? []).some((name) =>
|
|
102
|
+
["-", "_", "."].some((separator) =>
|
|
103
|
+
needle.startsWith(`${name.toLowerCase()}${separator}`),
|
|
104
|
+
),
|
|
105
|
+
)),
|
|
106
|
+
);
|
|
107
|
+
if (known) {
|
|
108
|
+
return catalogueName(known.id);
|
|
109
|
+
}
|
|
110
|
+
// Legacy ProgIds absent from the upstream catalogue remain recognizable.
|
|
111
|
+
if (/^safariurl/i.test(id)) {
|
|
112
|
+
return catalogueName("safari");
|
|
113
|
+
}
|
|
114
|
+
if (/^chromiumhtm/i.test(id)) {
|
|
115
|
+
return catalogueName("chromium");
|
|
116
|
+
}
|
|
117
|
+
if (/^ie\.http/i.test(id)) {
|
|
118
|
+
return "Internet Explorer";
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
/**
|
package/src/profile.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { exec } from "command-stream";
|
|
6
|
+
import { findBrowserSource } from "browser-commander";
|
|
6
7
|
|
|
7
8
|
const APPLICATION = "package-registry-manager";
|
|
8
9
|
const PROFILE = "browser-profile";
|
|
@@ -17,20 +18,26 @@ export function defaultBrowserProfile({
|
|
|
17
18
|
platform = process.platform,
|
|
18
19
|
env = process.env,
|
|
19
20
|
home = os.homedir(),
|
|
21
|
+
channel = "chrome",
|
|
20
22
|
} = {}) {
|
|
23
|
+
const source = findBrowserSource(channel);
|
|
24
|
+
const profile =
|
|
25
|
+
source?.controlProtocol && source.controlProtocol !== "cdp"
|
|
26
|
+
? `${PROFILE}-${source.id}`
|
|
27
|
+
: PROFILE;
|
|
21
28
|
const join = platform === "win32" ? path.win32.join : path.posix.join;
|
|
22
29
|
if (platform === "win32") {
|
|
23
30
|
const base = env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
24
|
-
return join(base, APPLICATION,
|
|
31
|
+
return join(base, APPLICATION, profile);
|
|
25
32
|
}
|
|
26
33
|
if (platform === "darwin") {
|
|
27
|
-
return join(home, "Library", "Application Support", APPLICATION,
|
|
34
|
+
return join(home, "Library", "Application Support", APPLICATION, profile);
|
|
28
35
|
}
|
|
29
36
|
// The XDG specification ignores relative values.
|
|
30
37
|
const state = path.posix.isAbsolute(env.XDG_STATE_HOME ?? "")
|
|
31
38
|
? env.XDG_STATE_HOME
|
|
32
39
|
: join(home, ".local", "state");
|
|
33
|
-
return join(state, APPLICATION,
|
|
40
|
+
return join(state, APPLICATION, profile);
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/** The profile location used before it moved out of the repository. */
|
package/src/sign-in-import.mjs
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
// printed, logged, or cached: they are only counted.
|
|
6
6
|
import * as commander from "browser-commander";
|
|
7
7
|
|
|
8
|
+
import { catalogueName } from "./browser-catalogue.mjs";
|
|
8
9
|
import { detectDefaultBrowser } from "./default-browser.mjs";
|
|
9
10
|
|
|
10
|
-
/** Browser Commander's cookie sources, should a release stop exporting them. */
|
|
11
|
-
const FALLBACK_SOURCES = ["chrome", "edge", "brave", "chromium", "firefox"];
|
|
12
11
|
/** `--browser-import` values that pick the source browser for the user. */
|
|
13
12
|
export const IMPORT_CHOICES = ["default", "auto"];
|
|
14
13
|
/** `--browser-import-scope` values. */
|
|
@@ -21,22 +20,9 @@ export const SIGN_IN_DOMAINS = {
|
|
|
21
20
|
pypi: ["pypi.org", "github.com"],
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
const SOURCE_NAMES = {
|
|
26
|
-
chrome: "Google Chrome",
|
|
27
|
-
edge: "Microsoft Edge",
|
|
28
|
-
brave: "Brave",
|
|
29
|
-
chromium: "Chromium",
|
|
30
|
-
firefox: "Firefox",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Every browser `--browser-import` can read, from Browser Commander. Safari,
|
|
35
|
-
* Opera, Vivaldi, and Arc follow once Browser Commander reads them
|
|
36
|
-
* (link-foundation/browser-commander#114).
|
|
37
|
-
*/
|
|
23
|
+
/** Every import id from Browser Commander's public catalogue. */
|
|
38
24
|
export function importSources() {
|
|
39
|
-
return [...
|
|
25
|
+
return [...commander.BROWSER_IDS];
|
|
40
26
|
}
|
|
41
27
|
|
|
42
28
|
/** The sign-in domains of a registry, or none. */
|
|
@@ -46,15 +32,12 @@ export function signInDomains(registry) {
|
|
|
46
32
|
|
|
47
33
|
/** The display name of a source browser. */
|
|
48
34
|
export function sourceName(browser) {
|
|
49
|
-
return
|
|
35
|
+
return catalogueName(browser);
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
/** The source id of a default-browser display name, when it can be imported. */
|
|
53
39
|
export function sourceId(displayName) {
|
|
54
|
-
|
|
55
|
-
([, name]) => name === displayName,
|
|
56
|
-
);
|
|
57
|
-
return entry && importSources().includes(entry[0]) ? entry[0] : undefined;
|
|
40
|
+
return importSources().find((id) => sourceName(id) === displayName);
|
|
58
41
|
}
|
|
59
42
|
|
|
60
43
|
/**
|
|
@@ -87,7 +70,12 @@ export async function findSignInSources(domains, options = {}) {
|
|
|
87
70
|
);
|
|
88
71
|
const found = [];
|
|
89
72
|
for (const browser of browsers) {
|
|
90
|
-
const profiles = await list({ browser }).catch(() =>
|
|
73
|
+
const profiles = await list({ browser }).catch((error) => {
|
|
74
|
+
if (options.verbose) {
|
|
75
|
+
console.error(`could not list ${browser} profiles: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
});
|
|
91
79
|
for (const profile of profiles) {
|
|
92
80
|
const matched = [];
|
|
93
81
|
for (const domain of domains) {
|
|
@@ -171,15 +159,15 @@ export async function chooseSignInSource(sources, domains, ask, auto) {
|
|
|
171
159
|
* Resolves `--browser-import default|auto` to an installed source browser:
|
|
172
160
|
* the system default browser, or for `auto` the first browser with a
|
|
173
161
|
* sign-in for `domains`. Throws when `default` names a browser Browser
|
|
174
|
-
* Commander cannot
|
|
162
|
+
* Commander cannot identify.
|
|
175
163
|
*/
|
|
176
164
|
export async function resolveImportChoice(choice, domains, options = {}) {
|
|
177
165
|
const preferred = await defaultSource(options);
|
|
178
166
|
if (choice === "default") {
|
|
179
167
|
if (!preferred) {
|
|
180
168
|
throw new Error(
|
|
181
|
-
"--browser-import default: the default browser cannot be imported
|
|
182
|
-
`${importSources().join(", ")}
|
|
169
|
+
"--browser-import default: the default browser is unknown or cannot be imported (Browser Commander sources: " +
|
|
170
|
+
`${importSources().join(", ")})`,
|
|
183
171
|
);
|
|
184
172
|
}
|
|
185
173
|
return { browser: preferred, profile: null };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Firefox-family browsers use Browser Commander's native WebDriver/BiDi API.
|
|
2
|
+
// Its real/CDP launcher and migration target writers do not support them yet.
|
|
3
|
+
import { launchWebDriver, readBrowserCookies } from "browser-commander";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assertDedicatedProfile,
|
|
7
|
+
resolveExecutable,
|
|
8
|
+
} from "./browser-catalogue.mjs";
|
|
9
|
+
import { ensureProfileIgnored } from "./profile.mjs";
|
|
10
|
+
import { IMPORT_CHOICES } from "./sign-in-import.mjs";
|
|
11
|
+
|
|
12
|
+
/** True only for a requested host or its subdomains, never a substring. */
|
|
13
|
+
export function matchesDomains(host, domains) {
|
|
14
|
+
const normalized = host.replace(/^\./, "").toLowerCase();
|
|
15
|
+
return domains.some(
|
|
16
|
+
(domain) => normalized === domain || normalized.endsWith(`.${domain}`),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The BiDi cookie shape; session cookies omit expiry. */
|
|
21
|
+
export function bidiCookie(cookie) {
|
|
22
|
+
const result = {
|
|
23
|
+
name: cookie.name,
|
|
24
|
+
value: { type: "string", value: cookie.value },
|
|
25
|
+
domain: cookie.domain,
|
|
26
|
+
path: cookie.path ?? "/",
|
|
27
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
28
|
+
secure: cookie.secure ?? false,
|
|
29
|
+
sameSite: (cookie.sameSite ?? "Lax").toLowerCase(),
|
|
30
|
+
};
|
|
31
|
+
if (cookie.expires > 0) {
|
|
32
|
+
result.expiry = cookie.expires;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Launches the selected installed Firefox variant and exposes the common page API. */
|
|
38
|
+
export async function connectWebDriverAutomation(
|
|
39
|
+
{ browser, profile, verbose, domains },
|
|
40
|
+
dependencies = {},
|
|
41
|
+
) {
|
|
42
|
+
// Validation happens before profile creation, including programmatic callers.
|
|
43
|
+
if (
|
|
44
|
+
browser.attach ||
|
|
45
|
+
browser.restrictions.length ||
|
|
46
|
+
Object.keys(browser.preferences).length ||
|
|
47
|
+
(browser.import && browser.importScope === "full")
|
|
48
|
+
) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`unsupported Firefox launch options for ${browser.channel}; use --browser-import-scope domains without snapshot, preferences or restrictions`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (browser.import && domains.length === 0) {
|
|
54
|
+
throw new Error("a domains import needs the registry's sign-in domains");
|
|
55
|
+
}
|
|
56
|
+
const executablePath = await (
|
|
57
|
+
dependencies.resolveExecutable ?? resolveExecutable
|
|
58
|
+
)({
|
|
59
|
+
channel: browser.channel.toLowerCase(),
|
|
60
|
+
executablePath: browser.executable,
|
|
61
|
+
});
|
|
62
|
+
assertDedicatedProfile(profile);
|
|
63
|
+
await ensureProfileIgnored(profile, { verbose });
|
|
64
|
+
const connection = await (dependencies.launchWebDriver ?? launchWebDriver)({
|
|
65
|
+
browser: "firefox",
|
|
66
|
+
executablePath,
|
|
67
|
+
userDataDir: profile,
|
|
68
|
+
headless: false,
|
|
69
|
+
bidi: true,
|
|
70
|
+
verbose,
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await connection.page.bidiCommand("storage.getCookies", {});
|
|
74
|
+
if (browser.import && !IMPORT_CHOICES.includes(browser.import.browser)) {
|
|
75
|
+
const read = dependencies.readCookies ?? readBrowserCookies;
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const domain of domains) {
|
|
78
|
+
const cookies = await read({
|
|
79
|
+
browser: browser.import.browser,
|
|
80
|
+
profile: browser.import.profile ?? undefined,
|
|
81
|
+
domainFilter: domain,
|
|
82
|
+
cache: false,
|
|
83
|
+
});
|
|
84
|
+
for (const cookie of cookies) {
|
|
85
|
+
const key = JSON.stringify([cookie.domain, cookie.path, cookie.name]);
|
|
86
|
+
if (!matchesDomains(cookie.domain, domains) || seen.has(key)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(key);
|
|
90
|
+
await connection.page.bidiCommand("storage.setCookie", {
|
|
91
|
+
cookie: bidiCookie(cookie),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
await connection.close().catch(() => {});
|
|
98
|
+
throw new Error(
|
|
99
|
+
`could not prepare ${browser.channel} sign-in cookies (WebDriver BiDi storage is required)`,
|
|
100
|
+
{ cause: error },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
goto: (url) => connection.page.goto(url),
|
|
105
|
+
evaluate: (script) => connection.page.evaluate(script),
|
|
106
|
+
close: () => connection.close(),
|
|
107
|
+
async clearCookies(names) {
|
|
108
|
+
const { cookies } = await connection.page.bidiCommand(
|
|
109
|
+
"storage.getCookies",
|
|
110
|
+
{},
|
|
111
|
+
);
|
|
112
|
+
for (const cookie of cookies) {
|
|
113
|
+
if (matchesDomains(cookie.domain, names)) {
|
|
114
|
+
await connection.page.bidiCommand("storage.deleteCookies", {
|
|
115
|
+
filter: {
|
|
116
|
+
name: cookie.name,
|
|
117
|
+
domain: cookie.domain,
|
|
118
|
+
path: cookie.path,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|