package-registry-manager 0.22.7 → 0.22.8

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.
@@ -0,0 +1,259 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { inspectRepository } from "./discovery.mjs";
5
+ import { githubServices } from "./github.mjs";
6
+ import { probeRegistryState } from "./registry-state.mjs";
7
+ import { RELEASE_FAILURE_PATTERNS } from "./publishing-policy.mjs";
8
+ import { credentialPolicy } from "./credential-cycle.mjs";
9
+ import { identityCommand } from "./repository-identity.mjs";
10
+
11
+ /** Only files used by existing discovery; no full repository clones for scans. */
12
+ export const SCAN_MATCHES = [
13
+ ...[
14
+ "package.json",
15
+ "Cargo.toml",
16
+ "pyproject.toml",
17
+ "setup.py",
18
+ "go.mod",
19
+ "pom.xml",
20
+ "build.gradle",
21
+ "build.gradle.kts",
22
+ "composer.json",
23
+ "jsr.json",
24
+ "deno.json",
25
+ "*.csproj",
26
+ "*.fsproj",
27
+ "*.vbproj",
28
+ "*.gemspec",
29
+ "Dockerfile*",
30
+ "Containerfile*",
31
+ "docker-compose*.yml",
32
+ "docker-compose*.yaml",
33
+ "compose*.yml",
34
+ "compose*.yaml",
35
+ ].map((name) => `**/${name}`),
36
+ ".github/workflows/*.yml",
37
+ ".github/workflows/*.yaml",
38
+ ".package-registry-manager.json",
39
+ ];
40
+
41
+ export function packageFindings(inspection) {
42
+ const slug = `${inspection.repository.github_owner}/${inspection.repository.github_repository}`;
43
+ return inspection.packages
44
+ .filter((item) => item.publishable)
45
+ .flatMap((item) => {
46
+ const common = {
47
+ repository: slug,
48
+ registry: item.registry,
49
+ package: item.name,
50
+ manifest: item.manifest,
51
+ };
52
+ const findings = [];
53
+ if (item.exists_on_registry === false) {
54
+ findings.push({ ...common, type: "unpublished" });
55
+ }
56
+ if (
57
+ item.exists_on_registry === true &&
58
+ (item.trusted_publishing === false ||
59
+ credentialPolicy(item.registry).mode === "token")
60
+ ) {
61
+ findings.push({
62
+ ...common,
63
+ type: "published-without-trusted-publishing",
64
+ });
65
+ }
66
+ const others = (item.configured_publishers ?? []).filter(
67
+ (publisher) =>
68
+ publisher.repository.toLowerCase() !== slug.toLowerCase(),
69
+ );
70
+ if (others.length) {
71
+ findings.push({
72
+ ...common,
73
+ type: "trusted-publisher-other-repository",
74
+ evidence: others,
75
+ });
76
+ }
77
+ return findings;
78
+ });
79
+ }
80
+
81
+ async function writeSnapshot(root, slug, files) {
82
+ for (const file of files) {
83
+ if (
84
+ typeof file.path !== "string" ||
85
+ file.path.includes("\\") ||
86
+ file.path.includes(":") ||
87
+ file.path
88
+ .split("/")
89
+ .some(
90
+ (part) => !part || part === "." || part === ".." || part === ".git",
91
+ )
92
+ ) {
93
+ throw new Error("unsafe repository file path");
94
+ }
95
+ if (typeof file.content !== "string") {
96
+ throw new Error("gh-manager returned no file content");
97
+ }
98
+ const target = path.join(root, file.path);
99
+ await mkdir(path.dirname(target), { recursive: true });
100
+ await writeFile(target, file.content);
101
+ }
102
+ await mkdir(path.join(root, ".git"), { recursive: true });
103
+ await writeFile(
104
+ path.join(root, ".git/config"),
105
+ `[remote "origin"]\n url = https://github.com/${slug}.git\n`,
106
+ );
107
+ }
108
+
109
+ /** Keep snapshots alive only for the callback; cleanup also runs after errors. */
110
+ export async function withAccountScan(options, callback) {
111
+ if (
112
+ Boolean(options.org) === Boolean(options.user) ||
113
+ !/^[\w.-]+$/.test(options.org ?? options.user)
114
+ ) {
115
+ throw new Error("choose exactly one valid --org or --user");
116
+ }
117
+ const github = options.github ?? githubServices(options);
118
+ const directory = await mkdtemp(path.join(os.tmpdir(), "prm-account-"));
119
+ try {
120
+ const repositories = await github.repos.list({
121
+ org: options.org,
122
+ user: options.user,
123
+ });
124
+ const scan = {
125
+ schema_version: 1,
126
+ account: options.org ?? options.user,
127
+ repositories: [],
128
+ findings: [],
129
+ };
130
+ for (const [index, repo] of repositories.entries()) {
131
+ const slug = repo.full_name;
132
+ if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) {
133
+ throw new Error("gh-manager returned an invalid repository name");
134
+ }
135
+ try {
136
+ const root = path.join(directory, String(index));
137
+ await writeSnapshot(
138
+ root,
139
+ slug,
140
+ await github.repos.files(slug, {
141
+ match: SCAN_MATCHES,
142
+ content: true,
143
+ ref: repo.default_branch,
144
+ }),
145
+ );
146
+ const discovered = await inspectRepository(root, {
147
+ includeSkipped: options.verbose,
148
+ });
149
+ const inspection = options.offline
150
+ ? discovered
151
+ : await (options.probe ?? probeRegistryState)(discovered, {
152
+ ...options,
153
+ runIdentity: async (program, args) =>
154
+ program === "gh"
155
+ ? slug
156
+ : identityCommand(program, args, {
157
+ ...options,
158
+ repository: root,
159
+ }),
160
+ repository: root,
161
+ });
162
+ scan.repositories.push({
163
+ repository: slug,
164
+ default_branch: repo.default_branch,
165
+ inspection,
166
+ });
167
+ scan.findings.push(...packageFindings(inspection));
168
+ } catch (error) {
169
+ scan.repositories.push({ repository: slug, error: error.message });
170
+ }
171
+ }
172
+ if (!options.offline) {
173
+ let failures;
174
+ if (options.org) {
175
+ failures = await github.runs.failures({
176
+ org: options.org,
177
+ grep: RELEASE_FAILURE_PATTERNS,
178
+ });
179
+ } else {
180
+ failures = [];
181
+ for (const repo of repositories) {
182
+ const [latest] = await github.runs.list(repo.full_name, {
183
+ branch: repo.default_branch,
184
+ limit: 1,
185
+ });
186
+ if (latest?.conclusion === "failure") {
187
+ const matches = await github.runs.logs(latest.id, {
188
+ repo: repo.full_name,
189
+ grep: RELEASE_FAILURE_PATTERNS,
190
+ });
191
+ if (matches.length) {
192
+ failures.push({
193
+ repository: repo.full_name,
194
+ runUrl: latest.html_url,
195
+ matches,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+ scan.findings.push(
202
+ ...failures.map((evidence) => ({
203
+ repository: evidence.repository,
204
+ type: "release-failing",
205
+ evidence,
206
+ })),
207
+ );
208
+ }
209
+ return await callback(scan, github);
210
+ } finally {
211
+ await rm(directory, { recursive: true, force: true });
212
+ }
213
+ }
214
+
215
+ export async function scanRepositories(options) {
216
+ return withAccountScan(options, (scan) => {
217
+ for (const item of scan.repositories) {
218
+ if (item.inspection) {
219
+ item.inspection.repository.root = `https://github.com/${item.repository}`;
220
+ }
221
+ }
222
+ return scan;
223
+ });
224
+ }
225
+
226
+ export function renderScan(scan) {
227
+ const rows = ["Repository\tRegistry\tPackage\tFinding"];
228
+ for (const item of scan.findings) {
229
+ rows.push(
230
+ `${item.repository}\t${item.registry ?? "-"}\t${item.package ?? "-"}\t${item.type.replaceAll("-", " ")}`,
231
+ );
232
+ }
233
+ for (const item of scan.repositories.filter((repo) => repo.error)) {
234
+ rows.push(`${item.repository}\t-\t-\tscan failed: ${item.error}`);
235
+ }
236
+ for (const repo of scan.repositories) {
237
+ for (const pkg of repo.inspection?.packages ?? []) {
238
+ if (
239
+ !pkg.publishable ||
240
+ scan.findings.some(
241
+ (finding) =>
242
+ finding.repository === repo.repository &&
243
+ finding.package === pkg.name &&
244
+ finding.registry === pkg.registry,
245
+ )
246
+ ) {
247
+ continue;
248
+ }
249
+ const status =
250
+ pkg.exists_on_registry === undefined
251
+ ? "registry state unknown"
252
+ : pkg.trusted_publishing === true
253
+ ? "published with trusted publishing"
254
+ : "published; trusted publishing unknown";
255
+ rows.push(`${repo.repository}\t${pkg.registry}\t${pkg.name}\t${status}`);
256
+ }
257
+ }
258
+ return `${rows.join("\n")}\n`;
259
+ }
@@ -0,0 +1,203 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFile, rm, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { withAccountScan, renderScan } from "./organization-scan.mjs";
5
+ import { buildPlans } from "./plan.mjs";
6
+ import { probeEnvironment } from "./prerequisites.mjs";
7
+ import { executePlans } from "./setup-batch.mjs";
8
+ import { secretName } from "./ci-credential-cycle.mjs";
9
+ import { TOKEN_PROVIDERS } from "./credential-browser.mjs";
10
+
11
+ async function prepareRepository(item) {
12
+ const root = item.inspection.repository.root;
13
+ await rm(`${root}/.git`, { recursive: true, force: true });
14
+ const commands = [
15
+ ["init", "--quiet"],
16
+ ["remote", "add", "origin", `https://github.com/${item.repository}.git`],
17
+ ["fetch", "--quiet", "--depth=1", "origin", item.default_branch],
18
+ ["checkout", "--quiet", "--force", "--detach", "FETCH_HEAD"],
19
+ ];
20
+ for (const args of commands) {
21
+ const result = spawnSync("git", args, { cwd: root, stdio: "pipe" });
22
+ if (result.status !== 0) {
23
+ throw new Error(`cannot prepare ${item.repository} for setup`);
24
+ }
25
+ }
26
+ }
27
+
28
+ /** Plan from API snapshots; fetch worktrees only for repositories being changed. */
29
+ export async function accountCommand(command, options, outputPlans) {
30
+ return withAccountScan(options, async (scan, github) => {
31
+ if (command === "inspect") {
32
+ for (const item of scan.repositories) {
33
+ if (item.inspection) {
34
+ item.inspection.repository.root = `https://github.com/${item.repository}`;
35
+ }
36
+ }
37
+ process.stdout.write(
38
+ options.format === "json"
39
+ ? `${JSON.stringify(scan, null, 2)}\n`
40
+ : renderScan(scan),
41
+ );
42
+ return scan;
43
+ }
44
+ const items = scan.repositories.filter((item) => item.inspection);
45
+ const errors = scan.repositories.filter((item) => item.error);
46
+ if (errors.length) {
47
+ console.error(renderScan({ repositories: errors, findings: [] }));
48
+ }
49
+ for (const item of items) {
50
+ const workflow = options.planOptions?.workflow;
51
+ if (workflow !== undefined) {
52
+ if (!/^[\w.-]+\.ya?ml$/.test(workflow)) {
53
+ throw new Error(
54
+ "--workflow must be a workflow file name in .github/workflows",
55
+ );
56
+ }
57
+ if (
58
+ !options.planOptions.addPublishJob &&
59
+ !(await stat(
60
+ path.join(
61
+ item.inspection.repository.root,
62
+ ".github/workflows",
63
+ workflow,
64
+ ),
65
+ ).then(
66
+ (metadata) => metadata.isFile(),
67
+ () => false,
68
+ ))
69
+ ) {
70
+ throw new Error(
71
+ `--workflow: ${item.repository} has no .github/workflows/${workflow}`,
72
+ );
73
+ }
74
+ }
75
+ }
76
+ const packages = items.flatMap((item) => item.inspection.packages);
77
+ const environment = await (options.probeEnvironment ?? probeEnvironment)({
78
+ offline: options.offline,
79
+ verbose: options.verbose,
80
+ npm: packages.some((item) => item.registry === "npm"),
81
+ python: packages.some((item) => item.registry === "pypi"),
82
+ });
83
+ let plans = items
84
+ .flatMap((item) => {
85
+ const relevant = scan.findings.filter(
86
+ (finding) => finding.repository === item.repository,
87
+ );
88
+ const inspection = {
89
+ ...item.inspection,
90
+ packages: item.inspection.packages.filter(
91
+ (pkg) =>
92
+ (!options.package || pkg.name === options.package) &&
93
+ (command === "plan" ||
94
+ relevant.some(
95
+ (finding) => !finding.package || finding.package === pkg.name,
96
+ )),
97
+ ),
98
+ };
99
+ return buildPlans(inspection, options.registries ?? [], {
100
+ ...options.planOptions,
101
+ environment,
102
+ }).filter((plan) => plan.package.publishable);
103
+ })
104
+ .sort(
105
+ (a, b) => Number(b.registry === "npm") - Number(a.registry === "npm"),
106
+ );
107
+ const display = structuredClone(plans);
108
+ for (const plan of display) {
109
+ plan.repository.root = `https://github.com/${plan.repository.github_owner}/${plan.repository.github_repository}`;
110
+ }
111
+ outputPlans(display, options.format);
112
+ if (options.package && !plans.length) {
113
+ throw new Error(`package '${options.package}' was not found`);
114
+ }
115
+ if (command === "plan" || plans.length === 0) {
116
+ if (errors.length) {
117
+ throw new Error(
118
+ "some repositories could not be scanned; see scan errors",
119
+ );
120
+ }
121
+ if (!plans.length && options.format !== "json") {
122
+ console.log("No package findings require setup.");
123
+ }
124
+ return [];
125
+ }
126
+ if (options.execute) {
127
+ for (const item of items.filter((item) =>
128
+ plans.some(
129
+ (plan) => plan.repository.root === item.inspection.repository.root,
130
+ ),
131
+ )) {
132
+ try {
133
+ await (options.prepareRepository ?? prepareRepository)(item);
134
+ } catch (error) {
135
+ item.error = error.message;
136
+ console.error(`${item.repository}: ${item.error}`);
137
+ plans = plans.filter(
138
+ (plan) => plan.repository.root !== item.inspection.repository.root,
139
+ );
140
+ }
141
+ }
142
+ if (!plans.length) {
143
+ throw new Error(
144
+ "some repositories could not be prepared; see scan errors",
145
+ );
146
+ }
147
+ }
148
+ const secretRepositories = {};
149
+ for (const plan of plans) {
150
+ const provider = TOKEN_PROVIDERS[plan.registry];
151
+ if (provider) {
152
+ const config = JSON.parse(
153
+ await readFile(
154
+ path.join(plan.repository.root, ".package-registry-manager.json"),
155
+ "utf8",
156
+ ).catch((error) => {
157
+ if (error.code === "ENOENT") {
158
+ return "{}";
159
+ }
160
+ throw error;
161
+ }),
162
+ );
163
+ if (config.tokens?.[plan.registry]?.level === "repo") {
164
+ continue;
165
+ }
166
+ const slug = `${plan.repository.github_owner}/${plan.repository.github_repository}`;
167
+ const name = secretName(
168
+ options.secretName ??
169
+ config.tokens?.[plan.registry]?.secret ??
170
+ plan.package.token_secrets?.[0] ??
171
+ provider.secret,
172
+ plan.registry,
173
+ slug,
174
+ );
175
+ const targets = (secretRepositories[`${plan.registry}:${name}`] ??= []);
176
+ if (!targets.includes(slug)) {
177
+ targets.push(slug);
178
+ }
179
+ }
180
+ }
181
+ const outcomes = await (options.executePlans ?? executePlans)(
182
+ plans,
183
+ {
184
+ ...options,
185
+ github,
186
+ accountScan: true,
187
+ browser: options.openWith ? options.browser : "automated",
188
+ quietBrowser: true,
189
+ secretRepositories,
190
+ repository: items[0]?.inspection.repository.root,
191
+ workflow: options.planOptions?.workflow,
192
+ publisherEnvironment: options.planOptions?.publisherEnvironment,
193
+ },
194
+ true,
195
+ );
196
+ if (scan.repositories.some((item) => item.error)) {
197
+ throw new Error(
198
+ "some repositories could not be scanned; see scan errors",
199
+ );
200
+ }
201
+ return outcomes;
202
+ });
203
+ }
@@ -0,0 +1,46 @@
1
+ /** Registry knowledge belongs to this consumer, never to gh-manager. */
2
+ export const AUTH_FAILURE_PATTERNS = Object.freeze({
3
+ npm: [
4
+ "\\bENEEDAUTH\\b",
5
+ "\\bE401\\b",
6
+ "\\bE403\\b",
7
+ "invalid-publisher",
8
+ "E404.*PUT|PUT.*E404",
9
+ "Access token expired or revoked",
10
+ ],
11
+ "crates-io": [
12
+ "No Trusted Publishing config found",
13
+ "invalid-publisher",
14
+ "token.*(?:invalid|expired|revoked)",
15
+ "authentication failed",
16
+ ],
17
+ pypi: [
18
+ "invalid-publisher",
19
+ "invalid.*(?:token|authentication)",
20
+ "403.*Forbidden",
21
+ ],
22
+ rubygems: ["API key.*(?:invalid|expired)", "invalid-publisher"],
23
+ nuget: ["API key.*(?:invalid|expired)", "invalid-publisher"],
24
+ jsr: ["invalid-publisher", "publishing.*(?:unauthorized|permission)"],
25
+ "docker-hub": [
26
+ "unauthorized: authentication required",
27
+ "denied: requested access",
28
+ "incorrect username or password",
29
+ ],
30
+ "maven-central": [
31
+ "401.*Unauthorized",
32
+ "403.*Forbidden",
33
+ "Invalid user token",
34
+ ],
35
+ "vscode-marketplace": ["Invalid.*(?:PAT|token)", "TF400813", "VSCE_PAT"],
36
+ "open-vsx": ["Invalid access token", "token.*(?:expired|invalid)"],
37
+ "chrome-web-store": [
38
+ "invalid_grant",
39
+ "invalid_client",
40
+ "invalid.*refresh.token",
41
+ ],
42
+ });
43
+
44
+ export const RELEASE_FAILURE_PATTERNS = Object.freeze([
45
+ ...new Set(Object.values(AUTH_FAILURE_PATTERNS).flat()),
46
+ ]);
@@ -23,6 +23,15 @@ const ENDPOINTS = {
23
23
  "PACKAGE_REGISTRY_MANAGER_DOCKER_HUB_API",
24
24
  "https://hub.docker.com/v2",
25
25
  ],
26
+ nuget: [
27
+ "PACKAGE_REGISTRY_MANAGER_NUGET_API",
28
+ "https://api.nuget.org/v3-flatcontainer",
29
+ ],
30
+ rubygems: [
31
+ "PACKAGE_REGISTRY_MANAGER_RUBYGEMS_API",
32
+ "https://rubygems.org/api/v1",
33
+ ],
34
+ jsr: ["PACKAGE_REGISTRY_MANAGER_JSR_API", "https://jsr.io"],
26
35
  };
27
36
 
28
37
  /** Returns the registry API base URL, honoring test overrides. */
@@ -54,6 +63,19 @@ export function registryStateUrl(packageInfo, env = process.env) {
54
63
  return `${base}/crates/${encodeURIComponent(name)}`;
55
64
  case "pypi":
56
65
  return `${base}/pypi/${encodeURIComponent(name)}/json`;
66
+ case "nuget":
67
+ return `${base}/${encodeURIComponent(name.toLowerCase())}/index.json`;
68
+ case "rubygems":
69
+ return `${base}/gems/${encodeURIComponent(name)}.json`;
70
+ case "jsr":
71
+ return `${base}/${name
72
+ .split("/")
73
+ .map((part) =>
74
+ part.startsWith("@")
75
+ ? `@${encodeURIComponent(part.slice(1))}`
76
+ : encodeURIComponent(part),
77
+ )
78
+ .join("/")}/meta.json`;
57
79
  default: {
58
80
  const [namespace, repository] = name.split("/");
59
81
  return `${base}/namespaces/${encodeURIComponent(namespace)}/repositories/${encodeURIComponent(repository)}`;
@@ -37,8 +37,27 @@ export async function executePlans(plans, options, summary = true) {
37
37
  if (plans.length === 0) {
38
38
  throw new Error("no publishable package manifests were found");
39
39
  }
40
+ const ready = [];
41
+ let blocked;
40
42
  for (const plan of plans) {
41
- validatePlan(plan, options.execute);
43
+ try {
44
+ validatePlan(plan, options.execute);
45
+ ready.push(plan);
46
+ } catch (error) {
47
+ if (!options.accountScan) {
48
+ throw error;
49
+ }
50
+ blocked ??= error;
51
+ console.error(
52
+ `${plan.repository.github_owner}/${plan.repository.github_repository}: ${plan.registry}: ${plan.package.name}: blocked: ${error.message}`,
53
+ );
54
+ }
55
+ }
56
+ if (blocked) {
57
+ if (ready.length) {
58
+ await executePlans(ready, options, summary);
59
+ }
60
+ throw blocked;
42
61
  }
43
62
  for (const plan of plans) {
44
63
  if (plan.mode === "complete") {
@@ -68,6 +87,32 @@ export async function executePlans(plans, options, summary = true) {
68
87
  plan.steps.some((step) => step.id === "add-publishing-workflow"),
69
88
  );
70
89
  if (missing.length) {
90
+ if (options.accountScan) {
91
+ const outcomes = [];
92
+ for (const root of new Set(missing.map((plan) => plan.repository.root))) {
93
+ const group = missing.filter((plan) => plan.repository.root === root);
94
+ const result = await (options.offerWorkflow ?? offerWorkflow)(group, {
95
+ ...options,
96
+ repository: root,
97
+ inspection: undefined,
98
+ prompt: options.prompt ?? prompt,
99
+ });
100
+ outcomes.push(
101
+ ...group.map((plan) => ({
102
+ registry: plan.registry,
103
+ package: plan.package.name,
104
+ repository: `${plan.repository.github_owner}/${plan.repository.github_repository}`,
105
+ ...result,
106
+ })),
107
+ );
108
+ }
109
+ const ready = plans.filter((plan) => !missing.includes(plan));
110
+ if (ready.length) {
111
+ outcomes.push(...(await executePlans(ready, options, false)));
112
+ }
113
+ printSummary(outcomes, summary);
114
+ return outcomes;
115
+ }
71
116
  const result = await offerWorkflow(missing, {
72
117
  ...options,
73
118
  prompt: options.prompt ?? prompt,
@@ -99,6 +144,9 @@ export async function executePlans(plans, options, summary = true) {
99
144
  for (const plan of plans) {
100
145
  const session = new SetupSession(plan, {
101
146
  ...options,
147
+ repository: options.accountScan
148
+ ? plan.repository.root
149
+ : options.repository,
102
150
  automation,
103
151
  domains,
104
152
  });
@@ -107,6 +155,7 @@ export async function executePlans(plans, options, summary = true) {
107
155
  outcomes.push({
108
156
  registry: plan.registry,
109
157
  package: plan.package.name,
158
+ repository: `${plan.repository.github_owner}/${plan.repository.github_repository}`,
110
159
  status: plan.steps.length === 0 ? "complete" : "configured",
111
160
  ...session.outcome,
112
161
  });
@@ -119,7 +168,7 @@ export async function executePlans(plans, options, summary = true) {
119
168
  package: plan.package.name,
120
169
  status: "failed",
121
170
  });
122
- failure = error;
171
+ failure ??= error;
123
172
  } finally {
124
173
  const steps = await session.cleanup({
125
174
  deferAuth: plans.length > 1,
@@ -130,7 +179,7 @@ export async function executePlans(plans, options, summary = true) {
130
179
  deferred.set(`${plan.registry}:${step.id}`, { session, step });
131
180
  }
132
181
  }
133
- if (failure) {
182
+ if (failure && !options.accountScan) {
134
183
  break;
135
184
  }
136
185
  }
@@ -168,7 +217,7 @@ function printSummary(outcomes, enabled) {
168
217
  console.log("\nSetup summary:");
169
218
  for (const item of outcomes) {
170
219
  console.log(
171
- `- ${item.registry}: ${item.package}: ${item.status}${item.url ? ` (${item.url})` : ""}`,
220
+ `- ${item.repository ? `${item.repository}: ` : ""}${item.registry}: ${item.package}: ${item.status}${item.secret_scope ? `; ${item.secret_scope} secret` : ""}${item.fallback_reason ? `; ${item.fallback_reason}` : ""}${item.url ? ` (${item.url})` : ""}`,
172
221
  );
173
222
  }
174
223
  }
package/src/setup.mjs CHANGED
@@ -681,6 +681,7 @@ export class SetupSession {
681
681
  * `--browser-import default|auto` itself, once it knows a sign-in is missing.
682
682
  */
683
683
  async automatedPage(browser = this.options.browserOptions, quiet = false) {
684
+ quiet ||= this.options.quietBrowser;
684
685
  if (!this.automation) {
685
686
  const domains = this.options.domains ?? signInDomains(this.plan.registry);
686
687
  this.automation = await connectAutomation({