package-registry-manager 0.22.0 → 0.22.3

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 CHANGED
@@ -35,6 +35,7 @@ currently shares its release version with the Rust implementation.
35
35
  outside any repository (`~/Library/Application Support` on macOS,
36
36
  `$XDG_STATE_HOME` or `~/.local/state` on Linux, `%LOCALAPPDATA%` on Windows,
37
37
  each followed by `package-registry-manager/browser-profile`) (#8).
38
+ - `browser-commander` is required at `^0.22.0`, its latest release (#28).
38
39
 
39
40
  ### Fixed
40
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.22.0",
3
+ "version": "0.22.3",
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/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/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 .",
@@ -39,13 +39,13 @@
39
39
  "pack:check": "npm pack --dry-run"
40
40
  },
41
41
  "dependencies": {
42
- "browser-commander": "^0.21.2",
43
- "command-stream": "^1.3.0",
42
+ "browser-commander": "^0.25.0",
43
+ "command-stream": "^1.5.0",
44
44
  "playwright": "^1.63.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@eslint/js": "^10.0.1",
48
- "eslint": "^10.11.0",
48
+ "eslint": "^10.12.0",
49
49
  "eslint-config-prettier": "^10.1.8",
50
50
  "jscpd": "^5.4.0",
51
51
  "prettier": "^3.9.9"
@@ -0,0 +1,133 @@
1
+ import { existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { BROWSER_IDS, findBrowserSource } from "browser-commander";
6
+
7
+ /** Catalogue entries reached through Browser Commander's public exports. */
8
+ export const browserSources = () => BROWSER_IDS.map(findBrowserSource);
9
+
10
+ /** The installed channels supported by Browser Commander's real launcher. */
11
+ export function launchChannels() {
12
+ return browserSources()
13
+ .filter((source) => source.controlProtocol === "cdp")
14
+ .flatMap((source) => [source.id, ...(source.aliases ?? [])]);
15
+ }
16
+
17
+ /** A readable name from the catalogue's application path or canonical alias. */
18
+ export function catalogueName(id) {
19
+ const source = findBrowserSource(id);
20
+ if (!source) {
21
+ return id;
22
+ }
23
+ const app = source.executables?.darwin?.[0]?.match(/\/([^/]+)\.app\//)?.[1];
24
+ // Preserve the established short name of Brave.
25
+ if (app) {
26
+ return app === "Brave Browser" ? "Brave" : app;
27
+ }
28
+ const label = [source.id, ...(source.aliases ?? [])]
29
+ .map((name) => name.replace(/^apple-/, ""))
30
+ .reduce((longest, name) => (name.length > longest.length ? name : longest));
31
+ return label
32
+ .replace(/^apple-/, "")
33
+ .split("-")
34
+ .map((word) =>
35
+ word.length <= 2
36
+ ? word.toUpperCase()
37
+ : word[0].toUpperCase() + word.slice(1),
38
+ )
39
+ .join(" ")
40
+ .replace("Duckduckgo", "DuckDuckGo");
41
+ }
42
+
43
+ /** Installed executables or existing profile roots; never reads cookie data. */
44
+ export function installedBrowsers(options = {}) {
45
+ const exists = options.exists ?? existsSync;
46
+ const platform = options.platform ?? process.platform;
47
+ const environment = options.environment ?? process.env;
48
+ const discovery = {
49
+ ...options,
50
+ platform,
51
+ environment:
52
+ platform === "win32"
53
+ ? Object.fromEntries(
54
+ Object.entries(environment).map(([key, value]) => [
55
+ key.toUpperCase(),
56
+ value,
57
+ ]),
58
+ )
59
+ : environment,
60
+ };
61
+ return browserSources()
62
+ .filter((source) =>
63
+ installedCandidates(source, discovery).some((candidate) =>
64
+ exists(candidate),
65
+ ),
66
+ )
67
+ .map((source) => source.id);
68
+ }
69
+
70
+ function installedCandidates(
71
+ source,
72
+ {
73
+ platform = process.platform,
74
+ homeDir = os.homedir(),
75
+ environment = process.env,
76
+ },
77
+ ) {
78
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
79
+ const variables = {
80
+ home: homeDir,
81
+ config: environment.XDG_CONFIG_HOME ?? pathApi.join(homeDir, ".config"),
82
+ appSupport: pathApi.join(homeDir, "Library", "Application Support"),
83
+ appData: environment.APPDATA ?? pathApi.join(homeDir, "AppData", "Roaming"),
84
+ localAppData:
85
+ environment.LOCALAPPDATA ?? pathApi.join(homeDir, "AppData", "Local"),
86
+ programFiles: environment.PROGRAMFILES,
87
+ programFilesX86: environment["PROGRAMFILES(X86)"],
88
+ };
89
+ const templates = [
90
+ ...(source.executables?.[platform] ?? []),
91
+ ...(source.roots?.[platform] ?? []),
92
+ ];
93
+ const candidates = templates.flatMap((template) => {
94
+ const match = /^\{(\w+)\}\/?(.*)$/.exec(template);
95
+ if (!match) {
96
+ return [template];
97
+ }
98
+ const base = variables[match[1]];
99
+ return base === undefined
100
+ ? []
101
+ : [pathApi.join(base, ...match[2].split("/"))];
102
+ });
103
+ for (const directory of (environment.PATH ?? "")
104
+ .split(platform === "win32" ? ";" : ":")
105
+ .filter(Boolean)) {
106
+ for (const name of source.executableNames ?? []) {
107
+ candidates.push(
108
+ pathApi.join(directory, platform === "win32" ? `${name}.exe` : name),
109
+ );
110
+ }
111
+ }
112
+ return candidates;
113
+ }
114
+
115
+ /** Discovery information included when an import or channel is invalid. */
116
+ export function installedDescription(options = {}) {
117
+ const found = installedBrowsers(options);
118
+ return `Installed browsers found: ${found.join(", ") || "none"}.`;
119
+ }
120
+
121
+ /** Reject unavailable engines before creating an automated profile. */
122
+ export function validateChannel(channel) {
123
+ const known = findBrowserSource(channel);
124
+ if (known?.controlProtocol === "cdp") {
125
+ return channel;
126
+ }
127
+ const reason = known
128
+ ? `${channel} requires ${known.controlProtocol ?? known.family} control, which browser-commander's real launcher does not support yet (https://github.com/link-foundation/browser-commander/issues/114)`
129
+ : `unknown --browser-channel '${channel}'`;
130
+ throw new Error(
131
+ `${reason}; choose from ${launchChannels().join(", ")}. ${installedDescription()}`,
132
+ );
133
+ }
@@ -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,7 +52,7 @@ 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
58
  importScope: parseImportScope(importScope, importFrom),
@@ -82,21 +84,18 @@ export function parseBrowserOptions({
82
84
  }
83
85
 
84
86
  /** Parses `<browser>[:profile]`, `default`, or `auto`. */
85
- export function parseImport(spec) {
87
+ export function parseImport(spec, discovery = {}) {
86
88
  const separator = spec.indexOf(":");
87
89
  const browser = separator === -1 ? spec : spec.slice(0, separator);
88
90
  const profile = separator === -1 ? null : spec.slice(separator + 1);
89
91
  const choice = IMPORT_CHOICES.includes(browser);
90
- if (
91
- !importBrowsers().includes(browser) ||
92
- profile === "" ||
93
- (choice && profile !== null)
94
- ) {
92
+ const source = findBrowserSource(browser);
93
+ if ((!source && !choice) || profile === "" || (choice && profile !== null)) {
95
94
  throw new Error(
96
- `--browser-import must be <${importSources().join("|")}>[:profile], default, or auto`,
95
+ `--browser-import must be <${importSources().join("|")}>[:profile], default, or auto. ${installedDescription(discovery)}`,
97
96
  );
98
97
  }
99
- return { browser, profile };
98
+ return { browser: source?.id ?? browser, profile };
100
99
  }
101
100
 
102
101
  /**
@@ -184,10 +183,7 @@ export function validateRestrictions(names) {
184
183
 
185
184
  /** The installed browser a snapshot copies for a launch channel. */
186
185
  export function snapshotBrowser(channel) {
187
- if (channel.startsWith("msedge")) {
188
- return "edge";
189
- }
190
- return ["brave", "chromium"].includes(channel) ? channel : "chrome";
186
+ return findBrowserSource(channel)?.id ?? channel;
191
187
  }
192
188
 
193
189
  /** Describes where forms are filled, for the browser prerequisite. */
package/src/cli.mjs CHANGED
@@ -1,19 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, realpathSync } from "node:fs";
3
+ import { readFileSync } from "node:fs";
4
4
  import { stat } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
- import { fileURLToPath } from "node:url";
8
7
  import { parseArgs } from "node:util";
9
8
 
9
+ import { launchChannels } from "./browser-catalogue.mjs";
10
10
  import { parseBrowserOptions } from "./browser-options.mjs";
11
+ import { isDirectExecution } from "./direct-execution.mjs";
11
12
  import { inspectRepository } from "./discovery.mjs";
12
13
  import { parseRegistry } from "./model.mjs";
13
14
  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";
19
+
20
+ export { isDirectExecution };
17
21
 
18
22
  const HELP = `Usage: package-registry-manager-js [global options] <command>
19
23
 
@@ -60,17 +64,16 @@ Setup options:
60
64
  --browser <default|automated> Open sign-in and approval pages in your
61
65
  default browser, or in the automated
62
66
  profile too (default: default)
63
- --browser-channel <channel> Installed browser channel: chrome, chromium,
64
- brave, msedge, msedge-beta, msedge-dev, or
65
- msedge-canary (default: chrome)
67
+ --browser-channel <channel> Installed browser channel (default: chrome):
68
+ ${launchChannels().join(", ")}
66
69
  --browser-executable <path> Installed browser executable to launch
67
70
  instead of the channel's
68
71
  --browser-profile <path> Dedicated automation profile, used to fill
69
72
  forms (default: per-user state directory)
70
73
  --browser-import <browser>[:<profile>]|default|auto
71
- Copy data from your chrome, edge, brave,
72
- chromium, or firefox profile into the
73
- automated profile first; default takes the
74
+ Copy data from your installed browser profile:
75
+ ${importSources().join(", ")}
76
+ into the automated profile; default takes the
74
77
  system default browser, auto the first
75
78
  browser signed in to the registry
76
79
  --browser-import-scope <full|domains>
@@ -421,22 +424,6 @@ function packageVersion() {
421
424
  return JSON.parse(readFileSync(manifest, "utf8")).version;
422
425
  }
423
426
 
424
- /**
425
- * Whether this module is the program node started. npm, npx, and global
426
- * installs start bins through a symlink in node_modules/.bin, while
427
- * import.meta.url names the real file, so both sides are resolved first.
428
- */
429
- export function isDirectExecution(moduleUrl, entryPath) {
430
- if (!entryPath) {
431
- return false;
432
- }
433
- try {
434
- return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(entryPath);
435
- } catch {
436
- return false;
437
- }
438
- }
439
-
440
427
  if (isDirectExecution(import.meta.url, process.argv[1])) {
441
428
  main().catch((error) => {
442
429
  process.stderr.write(`error: ${error.message}\n`);
@@ -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 known = KNOWN_BROWSERS.find(([pattern]) => pattern.test(id));
104
- return known?.[1];
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
  /**
@@ -0,0 +1,18 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ /**
5
+ * Whether this module is the program node started. npm, npx, and global
6
+ * installs start bins through a symlink in node_modules/.bin, while
7
+ * import.meta.url names the real file, so both sides are resolved first.
8
+ */
9
+ export function isDirectExecution(moduleUrl, entryPath) {
10
+ if (!entryPath) {
11
+ return false;
12
+ }
13
+ try {
14
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(entryPath);
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
@@ -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
- /** Display names of the source browsers, as the default-browser query names them. */
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 [...(commander.SUPPORTED_COOKIE_BROWSERS ?? FALLBACK_SOURCES)];
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 SOURCE_NAMES[browser] ?? browser;
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
- const entry = Object.entries(SOURCE_NAMES).find(
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 read yet, such as Safari.
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 yet (Browser Commander reads " +
182
- `${importSources().join(", ")}; see link-foundation/browser-commander#114)`,
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 };