package-registry-manager 0.22.3 → 0.22.5

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/README.md CHANGED
@@ -10,12 +10,15 @@ browser, filling forms in a dedicated visible browser profile.
10
10
  npx package-registry-manager inspect --repository /path/to/repository
11
11
  npx package-registry-manager plan --repository /path/to/repository --format json
12
12
  npx package-registry-manager setup --repository /path/to/repository --registry npm
13
+ npx package-registry-manager setup --repository /path/to/repository --all --execute
13
14
  ```
14
15
 
15
16
  The package installs the command as both `package-registry-manager` and
16
17
  `package-registry-manager-js`; use the second name when the Rust command of the
17
18
  same name is also on your `PATH`.
18
19
 
19
- Setup is a dry run unless `--execute` is present, and it never publishes an
20
- artifact. See the repository README for all registry walkthroughs and safety
21
- details.
20
+ Setup is a dry run unless `--execute` is present. It can publish the first
21
+ version after confirmation; later versions publish through CI. Missing CI
22
+ publishing jobs are offered in a draft pull request, and PyPI builds select an
23
+ interpreter satisfying `requires-python`. See the repository README for the
24
+ registry walkthroughs and safety details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.22.3",
3
+ "version": "0.22.5",
4
4
  "description": "Inspect repositories and guide secure package-registry setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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/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",
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 && node --check src/python.mjs && node --check src/setup-batch.mjs && node --check src/workflow-proposal.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,12 +36,14 @@
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.25.0",
43
- "command-stream": "^1.5.0",
44
- "playwright": "^1.63.0"
43
+ "browser-commander": "^0.26.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",
@@ -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: "playwright",
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
- profile,
81
- verbose = false,
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 launchRealBrowser(
91
- launchOptions(browser, profile, verbose, domains),
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.browser.close(),
106
+ close: () => connection.close(),
97
107
  async clearCookies(names) {
98
108
  for (const name of names) {
99
109
  const escaped = name.replaceAll(".", "\\.");
@@ -1,4 +1,5 @@
1
- import { existsSync } from "node:fs";
1
+ import { constants, existsSync, realpathSync } from "node:fs";
2
+ import { access } from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
 
@@ -7,10 +8,10 @@ import { BROWSER_IDS, findBrowserSource } from "browser-commander";
7
8
  /** Catalogue entries reached through Browser Commander's public exports. */
8
9
  export const browserSources = () => BROWSER_IDS.map(findBrowserSource);
9
10
 
10
- /** The installed channels supported by Browser Commander's real launcher. */
11
+ /** Catalogue channels with a launch control protocol. */
11
12
  export function launchChannels() {
12
13
  return browserSources()
13
- .filter((source) => source.controlProtocol === "cdp")
14
+ .filter((source) => ["cdp", "bidi"].includes(source.controlProtocol))
14
15
  .flatMap((source) => [source.id, ...(source.aliases ?? [])]);
15
16
  }
16
17
 
@@ -121,13 +122,88 @@ export function installedDescription(options = {}) {
121
122
  /** Reject unavailable engines before creating an automated profile. */
122
123
  export function validateChannel(channel) {
123
124
  const known = findBrowserSource(channel);
124
- if (known?.controlProtocol === "cdp") {
125
+ if (["cdp", "bidi"].includes(known?.controlProtocol)) {
125
126
  return channel;
126
127
  }
127
128
  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
+ ? `${channel} has no launch control protocol supported by this manager${known.family === "safari" ? " (Safari setup: https://github.com/link-foundation/browser-commander/issues/126)" : ""}`
129
130
  : `unknown --browser-channel '${channel}'`;
130
131
  throw new Error(
131
132
  `${reason}; choose from ${launchChannels().join(", ")}. ${installedDescription()}`,
132
133
  );
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
+ }
@@ -55,7 +55,11 @@ export function parseBrowserOptions({
55
55
  channel: attach === "extension" ? channel : validateChannel(channel),
56
56
  executable: executable ? path.resolve(executable) : null,
57
57
  import: importFrom ? parseImport(importFrom) : null,
58
- importScope: parseImportScope(importScope, importFrom),
58
+ importScope:
59
+ (importScope === undefined || importScope === null) &&
60
+ findBrowserSource(channel)?.controlProtocol === "bidi"
61
+ ? "domains"
62
+ : parseImportScope(importScope, importFrom),
59
63
  attach: attach ? parseAttach(attach) : null,
60
64
  preferences: parsePreferences(preferences),
61
65
  restrictions: validateRestrictions(restrictions),
@@ -80,9 +84,34 @@ export function parseBrowserOptions({
80
84
  "--browser-attach extension cannot be combined with --browser-executable, --browser-pref, or --browser-restriction",
81
85
  );
82
86
  }
87
+ validateEngineOptions(options);
83
88
  return options;
84
89
  }
85
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
+
86
115
  /** Parses `<browser>[:profile]`, `default`, or `auto`. */
87
116
  export function parseImport(spec, discovery = {}) {
88
117
  const separator = spec.indexOf(":");
package/src/cli.mjs CHANGED
@@ -14,7 +14,11 @@ import { parseRegistry } from "./model.mjs";
14
14
  import { buildPlans } from "./plan.mjs";
15
15
  import { probeEnvironment, renderPrerequisites } from "./prerequisites.mjs";
16
16
  import { probeRegistryState } from "./registry-state.mjs";
17
- import { BROWSER_MODES, defaultBrowserProfile, executePlan } from "./setup.mjs";
17
+ import {
18
+ BROWSER_MODES,
19
+ defaultBrowserProfile,
20
+ executePlans,
21
+ } from "./setup.mjs";
18
22
  import { importSources } from "./sign-in-import.mjs";
19
23
 
20
24
  export { isDirectExecution };
@@ -25,6 +29,7 @@ Commands:
25
29
  inspect Discover supported package manifests
26
30
  plan [--registry <registry>] Print ordered setup plans
27
31
  setup --registry <registry> Bootstrap or attach trusted publishing
32
+ setup --all Set up every publishable package in one session
28
33
 
29
34
  Global options:
30
35
  -h, --help Print this help
@@ -41,6 +46,8 @@ Plan and setup options:
41
46
  --workflow <file> Trusted-publisher workflow file in
42
47
  .github/workflows, when detection finds
43
48
  several or the wrong one
49
+ --add-publish-job Propose missing publishing jobs in a draft PR;
50
+ --workflow chooses its target file
44
51
  --environment <name> GitHub environment of the trusted publisher
45
52
  --package <name> Plan only this package, or select it for
46
53
  setup when a registry has several
@@ -104,11 +111,13 @@ export async function main(args = process.argv.slice(2)) {
104
111
  offline: { type: "boolean", default: false },
105
112
  "verify-release": { type: "boolean", default: false },
106
113
  workflow: { type: "string" },
114
+ "add-publish-job": { type: "boolean", default: false },
107
115
  environment: { type: "string" },
108
116
  "dry-run": { type: "boolean", default: false },
109
117
  registry: { type: "string", multiple: true },
110
118
  package: { type: "string" },
111
119
  execute: { type: "boolean", default: false },
120
+ all: { type: "boolean", default: false },
112
121
  yes: { type: "boolean", default: false },
113
122
  "no-browser": { type: "boolean", default: false },
114
123
  "open-with": { type: "string" },
@@ -175,6 +184,12 @@ export async function main(args = process.argv.slice(2)) {
175
184
  const environment = await probeEnvironment({
176
185
  offline: values.offline,
177
186
  verbose: values.verbose,
187
+ python: inspection.packages.some(
188
+ (item) =>
189
+ item.registry === "pypi" &&
190
+ item.publishable &&
191
+ (registries.length === 0 || registries.includes("pypi")),
192
+ ),
178
193
  npm: inspection.packages.some(
179
194
  (item) =>
180
195
  item.registry === "npm" &&
@@ -185,7 +200,12 @@ export async function main(args = process.argv.slice(2)) {
185
200
  const planOptions = {
186
201
  verifyRelease: values["verify-release"],
187
202
  manual: values.manual,
188
- workflow: await workflowOverride(repository, values.workflow),
203
+ workflow: await workflowOverride(
204
+ repository,
205
+ values.workflow,
206
+ values["add-publish-job"],
207
+ ),
208
+ addPublishJob: values["add-publish-job"],
189
209
  publisherEnvironment: nonEmpty(values.environment, "--environment"),
190
210
  environment,
191
211
  browser: browserSummary(command, values, browserOptions),
@@ -198,7 +218,10 @@ export async function main(args = process.argv.slice(2)) {
198
218
  outputPlans(plans, values.format);
199
219
  return;
200
220
  }
201
- if (registries.length !== 1) {
221
+ if (values.all && values.package) {
222
+ throw new Error("--all and --package are mutually exclusive");
223
+ }
224
+ if (!values.all && registries.length !== 1) {
202
225
  throw new Error("setup requires exactly one --registry <registry>");
203
226
  }
204
227
  if (values["dry-run"] && values.execute) {
@@ -215,26 +238,36 @@ export async function main(args = process.argv.slice(2)) {
215
238
  }
216
239
 
217
240
  const plans = buildPlans(inspection, registries, planOptions);
218
- const plan = selectPlan(plans, values.package);
219
- outputPlans([plan], values.format);
220
- await executePlan(plan, {
221
- repository,
222
- execute: values.execute,
223
- yes: values.yes,
224
- noBrowser: values["no-browser"],
225
- openWith: nonEmpty(values["open-with"], "--open-with"),
226
- keepSession: values["keep-session"],
227
- browser: values.browser,
228
- browserOptions,
229
- browserProfile: path.resolve(
230
- values["browser-profile"] ?? defaultBrowserProfile(),
231
- ),
232
- verbose: values.verbose,
233
- verifyRelease: values["verify-release"],
234
- });
241
+ const selected = values.all
242
+ ? plans.filter((plan) => plan.package.publishable)
243
+ : [selectPlan(plans, values.package)];
244
+ outputPlans(selected, values.format);
245
+ await executePlans(
246
+ selected,
247
+ {
248
+ inspection,
249
+ workflow: planOptions.workflow,
250
+ publisherEnvironment: planOptions.publisherEnvironment,
251
+ repository,
252
+ execute: values.execute,
253
+ yes: values.yes,
254
+ noBrowser: values["no-browser"],
255
+ openWith: nonEmpty(values["open-with"], "--open-with"),
256
+ keepSession: values["keep-session"],
257
+ browser: values.browser,
258
+ browserOptions,
259
+ browserProfile: path.resolve(
260
+ values["browser-profile"] ??
261
+ defaultBrowserProfile({ channel: browserOptions.channel }),
262
+ ),
263
+ verbose: values.verbose,
264
+ verifyRelease: values["verify-release"],
265
+ },
266
+ values.all,
267
+ );
235
268
  }
236
269
 
237
- async function workflowOverride(repository, workflow) {
270
+ async function workflowOverride(repository, workflow, adding = false) {
238
271
  if (workflow === undefined) {
239
272
  return undefined;
240
273
  }
@@ -248,7 +281,7 @@ async function workflowOverride(repository, workflow) {
248
281
  (metadata) => metadata.isFile(),
249
282
  () => false,
250
283
  );
251
- if (!found) {
284
+ if (!found && !adding) {
252
285
  throw new Error(`--workflow: .github/workflows/${workflow} does not exist`);
253
286
  }
254
287
  return workflow;
@@ -273,7 +306,10 @@ function browserSummary(command, values, browserOptions) {
273
306
  channel: browserOptions.channel,
274
307
  profile:
275
308
  values.browser === "automated" && !browserOptions.attach
276
- ? path.resolve(values["browser-profile"] ?? defaultBrowserProfile())
309
+ ? path.resolve(
310
+ values["browser-profile"] ??
311
+ defaultBrowserProfile({ channel: browserOptions.channel }),
312
+ )
277
313
  : undefined,
278
314
  import: browserOptions.import ?? undefined,
279
315
  attach: browserOptions.attach ?? undefined,
package/src/discovery.mjs CHANGED
@@ -178,6 +178,11 @@ function withPublisher(item, workflows, publishers) {
178
178
  const secrets = tokenSecrets(workflows, item.registry);
179
179
  const warnings = [
180
180
  ...publisher.warnings,
181
+ ...(!publisher.workflow && publisher.candidates.length === 0
182
+ ? [
183
+ `no workflow publishes ${item.name} to ${item.registry}; CI releases will not reach it`,
184
+ ]
185
+ : []),
181
186
  ...secrets.map((secret) => tokenSecretWarning(item.registry, secret)),
182
187
  ];
183
188
  if (warnings.length > 0) {
@@ -291,7 +296,17 @@ function parsePyproject(contents, manifest) {
291
296
  if (!name) {
292
297
  return null;
293
298
  }
294
- return packageInfo("pypi", name, tomlString(section, "version"), manifest);
299
+ const item = packageInfo(
300
+ "pypi",
301
+ name,
302
+ tomlString(section, "version"),
303
+ manifest,
304
+ );
305
+ const required = tomlString(section, "requires-python");
306
+ if (required) {
307
+ item.requires_python = required;
308
+ }
309
+ return item;
295
310
  }
296
311
 
297
312
  function parseDotnet(contents, manifestPath, manifest) {
package/src/index.mjs CHANGED
@@ -3,4 +3,4 @@ export { inspectRepository } from "./discovery.mjs";
3
3
  export { REGISTRIES, parseRegistry } from "./model.mjs";
4
4
  export { buildPlans, packageDirectory } from "./plan.mjs";
5
5
  export { ensureProfileIgnored } from "./profile.mjs";
6
- export { defaultBrowserProfile, executePlan } from "./setup.mjs";
6
+ export { defaultBrowserProfile, executePlan, executePlans } from "./setup.mjs";
package/src/plan.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { pagesSteps } from "./pages.mjs";
10
10
  import { planPrerequisites } from "./prerequisites.mjs";
11
11
  import { TRUSTED_REGISTRIES } from "./publishers.mjs";
12
+ import { applyPython } from "./python.mjs";
12
13
 
13
14
  const FLOWS = new Map([
14
15
  ["npm", npmFlow],
@@ -166,6 +167,27 @@ function flowPlan(inspection, packageInfo, options) {
166
167
  skipped_reason: `several workflows publish to ${packageInfo.registry} (${packageInfo.workflow_candidates.join(", ")}); pass --workflow <file> to choose the trusted publisher`,
167
168
  };
168
169
  }
170
+ if (
171
+ trusted &&
172
+ (!workflow ||
173
+ (options.addPublishJob &&
174
+ !packageInfo.workflow &&
175
+ !packageInfo.workflow_candidates?.length))
176
+ ) {
177
+ return {
178
+ ...basePlan(inspection, packageInfo, [
179
+ {
180
+ id: "add-publishing-workflow",
181
+ title: "Offer a publishing job in a reviewed pull request",
182
+ kind: "manual",
183
+ description:
184
+ "Add the missing publishing job to the workflow used by the other registries, on a new branch and pull request. Review and merge it, then re-run setup to attach the trusted publisher.",
185
+ confirm: true,
186
+ },
187
+ ]),
188
+ skipped_reason: `no workflow publishes ${packageInfo.name} to ${packageInfo.registry}; CI releases will not reach it; add a publishing job or pass --workflow <file> before attaching a trusted publisher`,
189
+ };
190
+ }
169
191
  const context = {
170
192
  directory: packageDirectory(packageInfo.manifest),
171
193
  slug,
@@ -213,6 +235,7 @@ function flowPlan(inspection, packageInfo, options) {
213
235
  plan.trusted_publisher.project = packageInfo.name;
214
236
  }
215
237
  }
238
+ applyPython(plan, options.environment?.python);
216
239
  return plan;
217
240
  }
218
241
 
@@ -1,6 +1,7 @@
1
1
  import { exec } from "command-stream";
2
2
 
3
3
  import { automatedDescription } from "./browser-options.mjs";
4
+ import { probePython } from "./python.mjs";
4
5
  import { getJson, registryEndpoint } from "./registry-state.mjs";
5
6
 
6
7
  /** The Node.js versions npm 11 runs on. */
@@ -154,6 +155,7 @@ export async function probeEnvironment(options = {}) {
154
155
  : null;
155
156
  return {
156
157
  offline,
158
+ python: options.python ? await probePython({ verbose }) : undefined,
157
159
  node: nodeVersion,
158
160
  npm: npmVersion?.code === 0 ? npmVersion.stdout.trim() : null,
159
161
  trustNpm: spec,
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, PROFILE);
31
+ return join(base, APPLICATION, profile);
25
32
  }
26
33
  if (platform === "darwin") {
27
- return join(home, "Library", "Application Support", APPLICATION, PROFILE);
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, PROFILE);
40
+ return join(state, APPLICATION, profile);
34
41
  }
35
42
 
36
43
  /** The profile location used before it moved out of the repository. */
@@ -15,7 +15,7 @@ const JOB_PATTERNS = new Map([
15
15
  ],
16
16
  [
17
17
  "pypi",
18
- /pypa\/gh-action-pypi-publish|\btwine\s+upload\b|\b(?:uv|poetry|hatch|flit|pdm)\s+publish\b/,
18
+ /pypa\/gh-action-pypi-publish|\b(?:python(?:3(?:\.\d+)?)?\s+-m\s+)?twine\s+upload\b|\b(?:uv|poetry|hatch|flit|pdm)\s+publish\b/,
19
19
  ],
20
20
  ]);
21
21