package-registry-manager 0.22.8 → 0.22.10

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
@@ -33,7 +33,25 @@ package-registry-manager setup --org link-foundation --all --execute --browser-i
33
33
  Scans read matched manifests/workflows through gh-manager. Account setup shares
34
34
  one browser, groups npm approvals, and uses CI evidence to preserve healthy tokens
35
35
  or verify replacements before revocation. `--secret-name '{REGISTRY}_TOKEN_{REPO}'`
36
- selects custom credential names. Rust requires Node.js and npm for its pinned
37
- GitHub CLI dependency.
36
+ selects custom credential names.
37
+
38
+ Account scans print progress to stderr (`[37/169] owner/repository`) and stream
39
+ text inspection results as each repository finishes. JSON inspection remains one
40
+ report on stdout. Repository, manifest, registry lookup, and release-log failures are
41
+ included in each repository's `error` and structured `errors` fields; valid
42
+ packages and later repositories are retained. The command exits nonzero after
43
+ printing all results, and `setup --org ORG --all` still plans the usable packages.
44
+ Release health checks only the latest default-branch push or manual run of each
45
+ detected publishing workflow; it downloads logs only when that run failed.
46
+ Expired logs retain the failed run's URL and conclusion alongside the error.
47
+ Account `plan` and `setup` JSON reports also include a `plans` array.
48
+
49
+ Without `--repository`, online commands prefer the current repository's
50
+ organization, then a sole organization membership, then the signed-in GitHub
51
+ user. Multiple memberships fall back to the user; select an organization with
52
+ `--org`. If GitHub account discovery is unavailable, the current directory is
53
+ inspected. Explicit `--repository` (including `--repository .`), `--ref`, and
54
+ local `--offline` commands keep local scope. Both CLIs' GitHub dependency needs
55
+ Node.js 22.12 or newer and npm; JavaScript checks the runtime before loading it.
38
56
 
39
57
  See the [bootstrap, wrapper and credential guide](../docs/registry-setup.md) for branch/PR setup, dual npm names, registry policies and integration limits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.22.8",
3
+ "version": "0.22.10",
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/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 && node --check src/repository-identity.mjs && node --check src/repository-repair.mjs && node --check src/manifest-proposal.mjs",
31
+ "syntax": "node --check src/cli.mjs && node --check src/cli-main.mjs && node --check src/node-version.mjs && node --check src/default-scope.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 && node --check src/repository-identity.mjs && node --check src/repository-repair.mjs && node --check src/manifest-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 .",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/29808e088ac8fd8374361920bbebab07a4d63ac6",
44
- "browser-commander": "0.27.0",
45
- "command-stream": "^2.0.0",
44
+ "browser-commander": "^0.31.0",
45
+ "command-stream": "^2.1.0",
46
46
  "playwright": "^1.63.0",
47
47
  "selenium-webdriver": "^4.50.0"
48
48
  },
@@ -0,0 +1,540 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { parseArgs } from "node:util";
6
+
7
+ import { launchChannels } from "./browser-catalogue.mjs";
8
+ import { parseBrowserOptions } from "./browser-options.mjs";
9
+ import { inspectReference } from "./bootstrap-reference.mjs";
10
+ import { inspectRepository } from "./discovery.mjs";
11
+ import { parseRegistry } from "./model.mjs";
12
+ import { buildPlans } from "./plan.mjs";
13
+ import { probeEnvironment, renderPrerequisites } from "./prerequisites.mjs";
14
+ import { probeRegistryState } from "./registry-state.mjs";
15
+ import {
16
+ BROWSER_MODES,
17
+ defaultBrowserProfile,
18
+ executePlans,
19
+ } from "./setup.mjs";
20
+ import { importSources } from "./sign-in-import.mjs";
21
+ import { accountCommand } from "./organization-setup.mjs";
22
+ import { defaultScope } from "./default-scope.mjs";
23
+
24
+ const HELP = `Usage: package-registry-manager-js [global options] <command>
25
+
26
+ Commands:
27
+ inspect Discover supported package manifests
28
+ plan [--registry <registry>] Print ordered setup plans
29
+ setup --registry <registry> Bootstrap or attach trusted publishing
30
+ setup --all Set up every publishable package in one session
31
+
32
+ Global options:
33
+ -h, --help Print this help
34
+ -V, --version Print the version
35
+ --repository <path> Inspect only this repository (default: widest scope)
36
+ --org <org> | --user <login> Scan repositories through gh-manager
37
+ --secret-name <template> Secret name; supports {REGISTRY}, {REPO}, {ORG}
38
+ --format <text|json> Output format (default: text)
39
+ --offline Do not look up packages on registries
40
+ --verbose Print command details and output, and
41
+ list the manifests inspection skipped
42
+
43
+ Plan and setup options:
44
+ --verify-release Also watch the release workflow and confirm
45
+ that the next version has provenance
46
+ --workflow <file> Trusted-publisher workflow file in
47
+ .github/workflows, when detection finds
48
+ several or the wrong one
49
+ --add-publish-job Propose missing publishing jobs in a draft PR;
50
+ --workflow chooses its target file
51
+ --environment <name> GitHub environment of the trusted publisher
52
+ --ref <branch|pr> Bootstrap a pushed branch or PR head before merge
53
+ --package <name> Plan only this package, or select it for
54
+ setup when a registry has several
55
+
56
+ Setup options:
57
+ --dry-run Print the flow without running it (default)
58
+ --execute Run the flow and open a visible browser
59
+ --yes Confirm publishing, secret changes, and
60
+ form submission in advance
61
+ --no-browser Print the setup URL instead
62
+ --open-with <app> Open sign-in and approval pages in this
63
+ application instead of the default browser,
64
+ such as "Google Chrome" (macOS) or firefox
65
+ --manual crates.io: create, use, and revoke the
66
+ first-publish token by hand instead of
67
+ through the crates.io API
68
+ --keep-session Stay signed in after setup: npm keeps its
69
+ session token in npm's user configuration
70
+ until npm logout; crates.io keeps only the
71
+ automated profile's browser session
72
+ --browser <default|automated> Open sign-in and approval pages in your
73
+ default browser, or in the automated
74
+ profile too (default: default)
75
+ --browser-channel <channel> Installed browser channel (default: chrome):
76
+ ${launchChannels().join(", ")}
77
+ --browser-executable <path> Installed browser executable to launch
78
+ instead of the channel's
79
+ --browser-profile <path> Dedicated automation profile, used to fill
80
+ forms (default: per-user state directory)
81
+ --browser-import <browser>[:<profile>]|default|auto
82
+ Copy data from your installed browser profile:
83
+ ${importSources().join(", ")}
84
+ into the automated profile; default takes the
85
+ system default browser, auto the first
86
+ browser signed in to the registry
87
+ --browser-import-scope <full|domains>
88
+ Import the whole profile, or only the
89
+ registry's sign-in cookies (default: full
90
+ for a named browser, domains otherwise)
91
+ --browser-attach <mode> Fill forms in your own browser instead:
92
+ snapshot[:<profile>] launches a temporary
93
+ copy of your profile, extension drives your
94
+ running browser through the Browser
95
+ Commander extension
96
+ --browser-pref <key=value> Browser preference for the automated
97
+ profile, such as intl.accept_languages=en;
98
+ may be repeated
99
+ --browser-restriction <name> Launch restriction or preset, such as
100
+ no-extensions; may be repeated
101
+ `;
102
+
103
+ export async function main(args = process.argv.slice(2)) {
104
+ const { values, positionals } = parseArgs({
105
+ args,
106
+ allowPositionals: true,
107
+ strict: true,
108
+ options: {
109
+ ref: { type: "string" },
110
+ repository: { type: "string" },
111
+ org: { type: "string" },
112
+ user: { type: "string" },
113
+ "secret-name": { type: "string" },
114
+ format: { type: "string", default: "text" },
115
+ verbose: { type: "boolean", default: false },
116
+ offline: { type: "boolean", default: false },
117
+ "verify-release": { type: "boolean", default: false },
118
+ workflow: { type: "string" },
119
+ "add-publish-job": { type: "boolean", default: false },
120
+ environment: { type: "string" },
121
+ "dry-run": { type: "boolean", default: false },
122
+ registry: { type: "string", multiple: true },
123
+ package: { type: "string" },
124
+ execute: { type: "boolean", default: false },
125
+ all: { type: "boolean", default: false },
126
+ yes: { type: "boolean", default: false },
127
+ "no-browser": { type: "boolean", default: false },
128
+ "open-with": { type: "string" },
129
+ "keep-session": { type: "boolean", default: false },
130
+ manual: { type: "boolean", default: false },
131
+ browser: { type: "string", default: "default" },
132
+ "browser-channel": { type: "string", default: "chrome" },
133
+ "browser-profile": { type: "string" },
134
+ "browser-executable": { type: "string" },
135
+ "browser-import": { type: "string" },
136
+ "browser-import-scope": { type: "string" },
137
+ "browser-attach": { type: "string" },
138
+ "browser-pref": { type: "string", multiple: true },
139
+ "browser-restriction": { type: "string", multiple: true },
140
+ help: { type: "boolean", short: "h", default: false },
141
+ version: { type: "boolean", short: "V", default: false },
142
+ },
143
+ });
144
+ if (values.help) {
145
+ process.stdout.write(HELP);
146
+ return;
147
+ }
148
+ if (values.version) {
149
+ process.stdout.write(`package-registry-manager ${packageVersion()}\n`);
150
+ return;
151
+ }
152
+ if (positionals.length !== 1) {
153
+ throw new Error("expected exactly one command: inspect, plan, or setup");
154
+ }
155
+ if (!["text", "json"].includes(values.format)) {
156
+ throw new Error("--format must be 'text' or 'json'");
157
+ }
158
+ if (!BROWSER_MODES.includes(values.browser)) {
159
+ throw new Error("--browser must be 'default' or 'automated'");
160
+ }
161
+ const browserOptions = parseBrowserOptions({
162
+ channel: values["browser-channel"],
163
+ executable: values["browser-executable"],
164
+ importFrom: values["browser-import"],
165
+ importScope: values["browser-import-scope"],
166
+ attach: values["browser-attach"],
167
+ preferences: values["browser-pref"],
168
+ restrictions: values["browser-restriction"],
169
+ profileGiven: values["browser-profile"] !== undefined,
170
+ });
171
+
172
+ if (!values.org && !values.user) {
173
+ Object.assign(values, await defaultScope(values));
174
+ }
175
+ const repository = path.resolve(values.repository ?? ".");
176
+ const command = positionals[0];
177
+ if (values.org || values.user) {
178
+ if (values.org && values.user) {
179
+ throw new Error("--org and --user are mutually exclusive");
180
+ }
181
+ if (values.repository !== undefined || values.ref) {
182
+ throw new Error("account scans cannot use --repository or --ref");
183
+ }
184
+ if (!["inspect", "plan", "setup"].includes(command)) {
185
+ throw new Error(`unknown command '${command}'`);
186
+ }
187
+ if (command === "setup" && (!values.all || values.package)) {
188
+ throw new Error("account setup requires --all without --package");
189
+ }
190
+ if (values["dry-run"] && values.execute) {
191
+ throw new Error("--dry-run and --execute are mutually exclusive");
192
+ }
193
+ if (values.yes && !values.execute) {
194
+ throw new Error("--yes requires --execute");
195
+ }
196
+ if (values["no-browser"] && !values.execute) {
197
+ throw new Error("--no-browser requires --execute");
198
+ }
199
+ if (values["open-with"] !== undefined && values["no-browser"]) {
200
+ throw new Error("--open-with and --no-browser are mutually exclusive");
201
+ }
202
+ if (!browserOptions.attach) {
203
+ browserOptions.import ??= { browser: "auto", profile: null };
204
+ }
205
+ return accountCommand(
206
+ command,
207
+ {
208
+ org: values.org,
209
+ user: values.user,
210
+ format: values.format,
211
+ offline: values.offline,
212
+ verbose: values.verbose,
213
+ registries: (values.registry ?? []).map(parseRegistry),
214
+ execute: values.execute,
215
+ yes: values.yes,
216
+ noBrowser: values["no-browser"],
217
+ secretName: values["secret-name"],
218
+ package: values.package,
219
+ browser: values.browser,
220
+ browserOptions,
221
+ browserProfile: path.resolve(
222
+ values["browser-profile"] ??
223
+ defaultBrowserProfile({ channel: browserOptions.channel }),
224
+ ),
225
+ openWith: nonEmpty(values["open-with"], "--open-with"),
226
+ keepSession: values["keep-session"],
227
+ planOptions: {
228
+ workflow: values.workflow,
229
+ publisherEnvironment: values.environment,
230
+ addPublishJob: values["add-publish-job"],
231
+ manual: values.manual,
232
+ verifyRelease: values["verify-release"],
233
+ },
234
+ },
235
+ outputPlans,
236
+ );
237
+ }
238
+ const discovered = values.ref
239
+ ? await inspectReference(repository, values.ref, {
240
+ includeSkipped: values.verbose,
241
+ })
242
+ : await inspectRepository(repository, { includeSkipped: values.verbose });
243
+ const inspection = values.offline
244
+ ? discovered
245
+ : await probeRegistryState(discovered, { verbose: values.verbose });
246
+ if (command === "inspect") {
247
+ outputInspection(inspection, values.format);
248
+ return;
249
+ }
250
+
251
+ const registries = (values.registry ?? []).map(parseRegistry);
252
+ if (!["plan", "setup"].includes(command)) {
253
+ throw new Error(`unknown command '${command}'`);
254
+ }
255
+ const environment = await probeEnvironment({
256
+ offline: values.offline,
257
+ verbose: values.verbose,
258
+ python: inspection.packages.some(
259
+ (item) =>
260
+ item.registry === "pypi" &&
261
+ item.publishable &&
262
+ (registries.length === 0 || registries.includes("pypi")),
263
+ ),
264
+ npm: inspection.packages.some(
265
+ (item) =>
266
+ item.registry === "npm" &&
267
+ item.publishable &&
268
+ (registries.length === 0 || registries.includes("npm")),
269
+ ),
270
+ });
271
+ const planOptions = {
272
+ ref: values.ref,
273
+ verifyRelease: values["verify-release"],
274
+ manual: values.manual,
275
+ workflow: await workflowOverride(
276
+ repository,
277
+ values.workflow,
278
+ values["add-publish-job"],
279
+ ),
280
+ addPublishJob: values["add-publish-job"],
281
+ publisherEnvironment: nonEmpty(values.environment, "--environment"),
282
+ environment,
283
+ browser: browserSummary(command, values, browserOptions),
284
+ };
285
+ if (command === "plan") {
286
+ const plans = filterPlans(
287
+ buildPlans(inspection, registries, planOptions),
288
+ values.package,
289
+ );
290
+ outputPlans(plans, values.format);
291
+ return;
292
+ }
293
+ if (values.all && values.package) {
294
+ throw new Error("--all and --package are mutually exclusive");
295
+ }
296
+ if (!values.all && registries.length !== 1) {
297
+ throw new Error("setup requires exactly one --registry <registry>");
298
+ }
299
+ if (values["dry-run"] && values.execute) {
300
+ throw new Error("--dry-run and --execute are mutually exclusive");
301
+ }
302
+ if (values.yes && !values.execute) {
303
+ throw new Error("--yes requires --execute");
304
+ }
305
+ if (values["no-browser"] && !values.execute) {
306
+ throw new Error("--no-browser requires --execute");
307
+ }
308
+ if (values["open-with"] !== undefined && values["no-browser"]) {
309
+ throw new Error("--open-with and --no-browser are mutually exclusive");
310
+ }
311
+
312
+ const plans = buildPlans(inspection, registries, planOptions);
313
+ const selected = values.all
314
+ ? plans.filter((plan) => plan.package.publishable)
315
+ : [selectPlan(plans, values.package)];
316
+ outputPlans(selected, values.format);
317
+ await executePlans(
318
+ selected,
319
+ {
320
+ inspection,
321
+ workflow: planOptions.workflow,
322
+ publisherEnvironment: planOptions.publisherEnvironment,
323
+ repository,
324
+ execute: values.execute,
325
+ yes: values.yes,
326
+ noBrowser: values["no-browser"],
327
+ openWith: nonEmpty(values["open-with"], "--open-with"),
328
+ keepSession: values["keep-session"],
329
+ secretName: values["secret-name"],
330
+ quietBrowser: selected.some((plan) =>
331
+ plan.steps.some((step) => step.id === "manage-registry-token"),
332
+ ),
333
+ browser: values.browser,
334
+ browserOptions,
335
+ browserProfile: path.resolve(
336
+ values["browser-profile"] ??
337
+ defaultBrowserProfile({ channel: browserOptions.channel }),
338
+ ),
339
+ verbose: values.verbose,
340
+ verifyRelease: values["verify-release"],
341
+ },
342
+ values.all,
343
+ );
344
+ }
345
+
346
+ async function workflowOverride(repository, workflow, adding = false) {
347
+ if (workflow === undefined) {
348
+ return undefined;
349
+ }
350
+ if (!/^[\w.-]+\.ya?ml$/.test(workflow)) {
351
+ throw new Error(
352
+ "--workflow must be a workflow file name in .github/workflows, such as release.yml",
353
+ );
354
+ }
355
+ const file = path.join(repository, ".github/workflows", workflow);
356
+ const found = await stat(file).then(
357
+ (metadata) => metadata.isFile(),
358
+ () => false,
359
+ );
360
+ if (!found && !adding) {
361
+ throw new Error(`--workflow: .github/workflows/${workflow} does not exist`);
362
+ }
363
+ return workflow;
364
+ }
365
+
366
+ function nonEmpty(value, option) {
367
+ if (value !== undefined && !value.trim()) {
368
+ throw new Error(`${option} must not be empty`);
369
+ }
370
+ return value?.trim();
371
+ }
372
+
373
+ function browserSummary(command, values, browserOptions) {
374
+ if (command === "setup" && values["no-browser"]) {
375
+ return { mode: "none" };
376
+ }
377
+ if (command !== "setup") {
378
+ return { mode: "default", channel: values["browser-channel"] };
379
+ }
380
+ return {
381
+ mode: values.browser,
382
+ channel: browserOptions.channel,
383
+ profile:
384
+ values.browser === "automated" && !browserOptions.attach
385
+ ? path.resolve(
386
+ values["browser-profile"] ??
387
+ defaultBrowserProfile({ channel: browserOptions.channel }),
388
+ )
389
+ : undefined,
390
+ import: browserOptions.import ?? undefined,
391
+ attach: browserOptions.attach ?? undefined,
392
+ };
393
+ }
394
+
395
+ /** Keeps the plans of `packageName`, or every plan without one. */
396
+ function filterPlans(plans, packageName) {
397
+ if (plans.length === 0) {
398
+ throw new Error("no matching package manifests were found");
399
+ }
400
+ if (!packageName) {
401
+ return plans;
402
+ }
403
+ const named = plans.filter((plan) => plan.package.name === packageName);
404
+ if (named.length === 0) {
405
+ throw new Error(`package '${packageName}' was not found`);
406
+ }
407
+ return named;
408
+ }
409
+
410
+ function selectPlan(plans, packageName) {
411
+ if (packageName) {
412
+ const plan = plans.find(
413
+ (candidate) => candidate.package.name === packageName,
414
+ );
415
+ if (!plan) {
416
+ throw new Error(
417
+ `package '${packageName}' was not found for this registry`,
418
+ );
419
+ }
420
+ return plan;
421
+ }
422
+ if (plans.length === 0) {
423
+ throw new Error("no matching package manifests were found");
424
+ }
425
+ const publishable = plans.filter((plan) => plan.package.publishable);
426
+ const candidates = publishable.length > 0 ? publishable : plans;
427
+ if (candidates.length > 1) {
428
+ throw new Error(
429
+ "multiple packages use this registry; select one with --package <name>",
430
+ );
431
+ }
432
+ return candidates[0];
433
+ }
434
+
435
+ function outputInspection(inspection, format) {
436
+ if (format === "json") {
437
+ process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
438
+ return;
439
+ }
440
+ process.stdout.write(`Repository: ${inspection.repository.root}\n`);
441
+ if (inspection.packages.length === 0) {
442
+ process.stdout.write("No supported package manifests found.\n");
443
+ }
444
+ for (const packageInfo of inspection.packages) {
445
+ const details = [
446
+ packageInfo.manifest,
447
+ packageInfo.publishable ? "publishable" : "not publishable",
448
+ ];
449
+ if (packageInfo.exists_on_registry !== undefined) {
450
+ details.push(
451
+ packageInfo.exists_on_registry ? "published" : "not published yet",
452
+ );
453
+ }
454
+ if (packageInfo.trusted_publishing) {
455
+ details.push("trusted publishing");
456
+ }
457
+ process.stdout.write(
458
+ `- ${packageInfo.registry}: ${packageInfo.name} (${details.join(", ")})\n`,
459
+ );
460
+ if (packageInfo.workflow && packageInfo.publishable) {
461
+ process.stdout.write(` workflow: ${publisherSummary(packageInfo)}\n`);
462
+ }
463
+ for (const warning of packageInfo.warnings ?? []) {
464
+ process.stdout.write(` warning: ${warning}\n`);
465
+ }
466
+ }
467
+ for (const item of inspection.skipped ?? []) {
468
+ process.stdout.write(`skipped ${item.manifest}: ${item.reason}\n`);
469
+ }
470
+ }
471
+
472
+ function publisherSummary({ workflow, workflow_jobs, environment }) {
473
+ const details = [];
474
+ if (workflow_jobs?.length) {
475
+ details.push(
476
+ `${workflow_jobs.length > 1 ? "jobs" : "job"} ${workflow_jobs.join(", ")}`,
477
+ );
478
+ }
479
+ if (environment) {
480
+ details.push(`environment ${environment}`);
481
+ }
482
+ return details.length > 0 ? `${workflow} (${details.join("; ")})` : workflow;
483
+ }
484
+
485
+ function outputPlans(plans, format) {
486
+ if (format === "json") {
487
+ process.stdout.write(`${JSON.stringify(plans, null, 2)}\n`);
488
+ return;
489
+ }
490
+ for (const plan of plans) {
491
+ const mode = plan.mode ? ` (${plan.mode})` : "";
492
+ process.stdout.write(`${plan.registry}: ${plan.package.name}${mode}\n`);
493
+ process.stdout.write(
494
+ ` credentials: ${plan.credential_policy.description}\n`,
495
+ );
496
+ if (plan.mode === "complete") {
497
+ process.stdout.write(" trusted publishing is already in use\n");
498
+ }
499
+ if (plan.skipped_reason) {
500
+ process.stdout.write(` skipped: ${plan.skipped_reason}\n`);
501
+ }
502
+ if (plan.trusted_publisher) {
503
+ process.stdout.write(
504
+ ` trusted publisher: ${publisherSummary({
505
+ ...plan.package,
506
+ workflow: plan.trusted_publisher.workflow,
507
+ environment: plan.trusted_publisher.environment,
508
+ workflow_jobs:
509
+ plan.trusted_publisher.workflow === plan.package.workflow
510
+ ? plan.package.workflow_jobs
511
+ : undefined,
512
+ })}\n`,
513
+ );
514
+ }
515
+ for (const warning of plan.package.warnings ?? []) {
516
+ process.stdout.write(` warning: ${warning}\n`);
517
+ }
518
+ for (const line of renderPrerequisites(plan.prerequisites ?? [])) {
519
+ process.stdout.write(`${line}\n`);
520
+ }
521
+ plan.steps.forEach((step, index) => {
522
+ const when = step.when ? ` [when ${step.when}]` : "";
523
+ process.stdout.write(` ${index + 1}. ${step.title}${when}\n`);
524
+ if (step.command) {
525
+ const cwd = step.cwd && step.cwd !== "." ? `(in ${step.cwd}) ` : "";
526
+ process.stdout.write(
527
+ ` $ ${cwd}${step.command.program} ${step.command.args.join(" ")}\n`,
528
+ );
529
+ }
530
+ if (step.url) {
531
+ process.stdout.write(` ${step.url}\n`);
532
+ }
533
+ });
534
+ }
535
+ }
536
+
537
+ function packageVersion() {
538
+ const manifest = new URL("../package.json", import.meta.url);
539
+ return JSON.parse(readFileSync(manifest, "utf8")).version;
540
+ }