rolldown-pnpm-config 1.0.11 → 2.0.0

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.
@@ -1,8 +1,11 @@
1
1
  import { bareVersion } from "../../semver-util.js";
2
+ import { WorkingDirectory } from "../cwd.js";
2
3
  import { evaluatePluginConfig } from "../evaluate.js";
4
+ import { printDoc } from "../render/print.js";
5
+ import { failureDoc, warningDoc } from "../render/report.js";
3
6
  import { discoverCatalogEntries } from "../discover.js";
4
7
  import { filterEntriesByCatalog, findConfigFiles, pickConfigCandidate } from "../select-file.js";
5
- import { detectCapabilities } from "../ui/env.js";
8
+ import { walkScreen, withProgress } from "../ui/screens.js";
6
9
  import { versionKeyOf } from "../version-key.js";
7
10
  import { buildEdits, entryEdits } from "../edits.js";
8
11
  import { buildInteropEdits, interopEntryChanged, makePeerFetcher, runInterop } from "../interop.js";
@@ -12,15 +15,16 @@ import { defaultPick, planEntry } from "../plan.js";
12
15
  import { parsePnpmGate, readConfigReleaseAge } from "../release-age.js";
13
16
  import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
14
17
  import { applyEdits } from "../rewrite.js";
15
- import { renderSummary } from "../summary.js";
18
+ import { summaryDoc } from "../summary.js";
16
19
  import { validateEdits } from "../validate.js";
17
20
  import { buildWalkItems } from "../walk-plan.js";
18
21
  import { findWorkspaceRoot, makeWorkspaceResolver } from "../workspace-resolve.js";
19
- import { Data, Effect, Option, Result } from "effect";
22
+ import { Console, Data, Effect, Option, Result, Runtime } from "effect";
20
23
  import { readFileSync, writeFileSync } from "node:fs";
21
24
  import { dirname } from "node:path";
22
- import { NodeServices } from "@effect/platform-node";
25
+ import { CliDoc, CliExit, CliInteractive, CliMessage, Doc } from "@effected/cli";
23
26
  import { Argument, Command, Flag } from "effect/cli";
27
+ import { CliUi } from "@effected/cli/ui";
24
28
  import { ReleaseAgeGate } from "@effected/npm";
25
29
 
26
30
  //#region src/cli/commands/upgrade.ts
@@ -29,7 +33,45 @@ import { ReleaseAgeGate } from "@effected/npm";
29
33
  *
30
34
  * @internal
31
35
  */
32
- var UpgradeError = class extends Data.TaggedError("UpgradeError") {};
36
+ var UpgradeError = class extends Data.TaggedError("UpgradeError") {
37
+ /** The failure report `CliRuntime.main` prints: the message, without the class name. */
38
+ [CliDoc]() {
39
+ return failureDoc(this.message);
40
+ }
41
+ };
42
+ /**
43
+ * An invalid flag combination: a usage error, so it exits 64 (BSD
44
+ * `EX_USAGE`) rather than the 1 a resolution failure exits with.
45
+ *
46
+ * @internal
47
+ */
48
+ var UpgradeUsageError = class extends Data.TaggedError("UpgradeUsageError") {
49
+ [Runtime.errorExitCode] = 64;
50
+ /** The failure report `CliRuntime.main` prints: the message, without the class name. */
51
+ [CliDoc]() {
52
+ return failureDoc(this.message);
53
+ }
54
+ };
55
+ /** The heading of `--check`'s resolution-failure report. @internal */
56
+ const CHECK_FAILED_LABEL = "Catalog check failed before drift could be evaluated (resolution error, not drift):";
57
+ function indent(message) {
58
+ return message.split("\n").map((l) => ` ${l}`).join("\n");
59
+ }
60
+ /**
61
+ * `--check`'s resolution failure: the run could not get far enough to say
62
+ * whether the catalogs drifted. It shares `--check`'s non-zero exit with
63
+ * drift, so its report names the failure family — a gate consuming the exit
64
+ * code reports every non-zero as "drifted", and without the label the CI log
65
+ * would blame drift for a typo'd package or an auth failure.
66
+ *
67
+ * @internal
68
+ */
69
+ var CheckFailedError = class extends Data.TaggedError("CheckFailedError") {
70
+ /** The failure report: the family label, then the underlying message indented. */
71
+ [CliDoc]() {
72
+ return failureDoc(`${CHECK_FAILED_LABEL}\n${indent(this.message)}`);
73
+ }
74
+ };
33
75
  /** Read a config file and statically discover its catalog entries. @internal */
34
76
  function readCatalogSource(file) {
35
77
  return Effect.gen(function* () {
@@ -67,9 +109,9 @@ const RESOLVE_CONCURRENCY = 12;
67
109
  * registry-sourced in another, and the two routes must neither share a version
68
110
  * list nor a gate exemption.
69
111
  *
70
- * @param onProgress - Optional callback invoked after each package resolves with
71
- * `(resolved, total)`. Useful for emitting CLI progress feedback. Called with
72
- * `(0, total)` before any work starts so callers can emit the initial banner.
112
+ * @param onProgress - Optional reporter run after each package resolves with
113
+ * `(resolved, total)`, for CLI progress feedback. Run with `(0, total)`
114
+ * before any work starts so callers can begin the phase.
73
115
  * @param workspace - Optional workspace-backed resolver. An entry whose
74
116
  * `source` is `"workspace"` resolves through it instead of the registry, and
75
117
  * is EXEMPT from the release-age gate: its next version is unpublished, so
@@ -85,12 +127,12 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress, workspac
85
127
  });
86
128
  const total = pairs.size;
87
129
  let resolved = 0;
88
- onProgress?.(0, total);
89
- return Effect.forEach([...pairs], ([key, { pkg, fromWorkspace }]) => Effect.gen(function* () {
130
+ const report = onProgress ?? (() => Effect.void);
131
+ return Effect.andThen(report(0, total), () => Effect.forEach([...pairs], ([key, { pkg, fromWorkspace }]) => Effect.gen(function* () {
90
132
  const routed = fromWorkspace && workspace !== void 0 ? workspace : resolver;
91
133
  const needTimes = !fromWorkspace && gate.ageMinutes > 0;
92
134
  const [vr, times] = yield* Effect.all([routed.versions(pkg).pipe(Effect.result), needTimes ? resolver.times(pkg).pipe(Effect.orElseSucceed(() => ({}))) : Effect.succeed({})], { concurrency: "unbounded" });
93
- onProgress?.(++resolved, total);
135
+ yield* report(++resolved, total);
94
136
  if (Result.isFailure(vr)) return [
95
137
  key,
96
138
  pkg,
@@ -113,22 +155,37 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress, workspac
113
155
  gated: new Map(rows.map(([key, , gated]) => [key, gated])),
114
156
  raw: new Map(rows.map(([key, , , raw]) => [key, raw])),
115
157
  unresolved: [...new Set(rows.filter(([, , , raw]) => raw.length === 0).map(([, pkg]) => pkg))]
116
- })));
158
+ }))));
117
159
  }
118
160
  /**
119
- * Write a resolve-progress line to stderr. Overwrites the previous line with
120
- * ANSI carriage return so the terminal shows a single updating counter instead
121
- * of a flood of lines. The initial call (resolved === 0) writes a newline so
122
- * the first subsequent overwrite lands on its own line.
123
- *
124
- * Only call when caps.interactive is true; this function is not gated itself.
161
+ * Adapt a progress-event reporter to `resolveGatedVersions`' `(resolved,
162
+ * total)` callback: the first call begins the resolve phase, each later one
163
+ * steps it, and the last finishes it, so the live view commits a
164
+ * "Resolved N packages" line as soon as resolution is done.
125
165
  *
126
166
  * @internal
127
167
  */
128
- function writeResolveProgress(resolved, total) {
129
- if (resolved === 0) process.stderr.write(`Resolving ${total} package${total === 1 ? "" : "s"}...\n`);
130
- else if (resolved === total) process.stderr.write(`\r Resolved ${resolved}/${total} \n`);
131
- else process.stderr.write(`\r Resolved ${resolved}/${total}`);
168
+ function resolveProgress(report) {
169
+ const noun = (n) => `${n} package${n === 1 ? "" : "s"}`;
170
+ const finished = (total) => report({
171
+ _tag: "Finished",
172
+ label: `Resolved ${noun(total)}`
173
+ });
174
+ return (resolved, total) => {
175
+ if (resolved === 0) {
176
+ const begin = report({
177
+ _tag: "Phase",
178
+ label: `Resolving ${noun(total)}`,
179
+ total
180
+ });
181
+ return total === 0 ? Effect.andThen(begin, finished(total)) : begin;
182
+ }
183
+ const step = report({
184
+ _tag: "Step",
185
+ done: resolved
186
+ });
187
+ return resolved === total ? Effect.andThen(step, finished(total)) : step;
188
+ };
132
189
  }
133
190
  /**
134
191
  * Non-interactive upgrade core: read the config, discover catalog entries,
@@ -275,7 +332,7 @@ function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
275
332
  * @internal
276
333
  */
277
334
  function nothingToUpgradeMessage(totalItems) {
278
- return totalItems === 0 ? "Nothing to upgrade — no catalog packages found.\n" : `Nothing to upgrade — ${totalItems} package(s) already up to date.\n`;
335
+ return totalItems === 0 ? "Nothing to upgrade — no catalog packages found." : `Nothing to upgrade — ${totalItems} package(s) already up to date.`;
279
336
  }
280
337
  /**
281
338
  * The message for packages the registry could not resolve. Almost always a
@@ -320,49 +377,51 @@ function projectDecisions(items, full) {
320
377
  }
321
378
  return out;
322
379
  }
323
- /** Build the colored preview summary string without writing. @internal */
380
+ /**
381
+ * Build the `--preview` projection without writing: the summary document, and
382
+ * the packages the registry could not resolve (which render as up to date and
383
+ * would otherwise be invisible in the projection).
384
+ *
385
+ * @internal
386
+ */
324
387
  function runUpgradePreview(opts) {
325
388
  return Effect.gen(function* () {
326
389
  const { source, entries } = yield* readCatalogSource(opts.file);
327
390
  const gate = yield* computeGate(source, opts.file, opts.resolver);
328
391
  const versions = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), void 0, opts.workspaceResolver);
329
392
  const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
330
- const text = renderSummary(projectDecisions(items, opts.full), void 0, { color: opts.color ?? false });
331
- return versions.unresolved.length > 0 ? `${text}\n⚠ ${unresolvedMessage(versions.unresolved)}` : text;
393
+ return {
394
+ doc: summaryDoc(projectDecisions(items, opts.full)),
395
+ unresolved: versions.unresolved
396
+ };
332
397
  });
333
398
  }
334
399
  /**
335
- * Map a check run's drift list to the process outcome. `--check` is a pure
336
- * gate: exit 0 when every entry is in sync, exit 1 when an `upgrade --yes`
337
- * would rewrite anything — the exit code IS the contract (a release
338
- * validation phase calls this), and it never writes.
400
+ * Map a check run's drift list to its report and exit code. `--check` is a
401
+ * pure gate: exit 0 when every entry is in sync, exit 1 when an
402
+ * `upgrade --yes` would rewrite anything — the exit code IS the contract (a
403
+ * release validation phase calls this), and it never writes. Drift is a
404
+ * finding, not a failure: the command succeeds and records the code with
405
+ * `CliExit.set`.
339
406
  *
340
407
  * @internal
341
408
  */
342
409
  function checkOutcome(changed) {
343
410
  if (changed.length === 0) return {
344
411
  exitCode: 0,
345
- text: "Catalogs are in sync.\n"
346
- };
347
- const list = changed.map((c) => ` ${c.name} (${c.source})`).join("\n");
348
- return {
349
- exitCode: 1,
350
- text: `Catalog drift detected in ${changed.length} package(s):\n${list}\nRun \`rolldown-pnpm-config upgrade --yes\` to apply.\n`
412
+ doc: [Doc.line("Catalogs are in sync.")]
351
413
  };
352
- }
353
- /**
354
- * Map a check run's FAILURE (a resolution failure or peer warning — an
355
- * UpgradeError, not drift) to the process outcome. Shares --check's single
356
- * non-zero exit code with drift, so the OUTPUT must name the failure family:
357
- * a gate consuming the exit code reports every non-zero as "drifted", and
358
- * without this label the CI log lies about a typo'd package or auth failure.
359
- *
360
- * @internal
361
- */
362
- function checkFailureOutcome(message) {
363
414
  return {
364
415
  exitCode: 1,
365
- text: `Catalog check failed before drift could be evaluated (resolution error, not drift):\n${message.split("\n").map((l) => ` ${l}`).join("\n")}\n`
416
+ doc: [
417
+ Doc.line(`Catalog drift detected in ${changed.length} package(s):`),
418
+ Doc.lines(changed.map((c) => ` ${c.name} (${c.source})`)),
419
+ Doc.line([
420
+ "Run ",
421
+ Doc.code("rolldown-pnpm-config upgrade --yes"),
422
+ " to apply."
423
+ ])
424
+ ]
366
425
  };
367
426
  }
368
427
  /** Project a drift row to its stable `--json` object: catalog/pkg/from/to?/source, camelCase, `to` omitted (never null) when unknown. */
@@ -375,44 +434,43 @@ function driftRowJson(row) {
375
434
  source: row.source
376
435
  };
377
436
  }
378
- /** Serialize one single-line JSON document, newline-terminated — the ONLY bytes `--json` puts on stdout. */
379
- function jsonLine(doc) {
380
- return `${JSON.stringify(doc)}\n`;
381
- }
382
437
  /**
383
438
  * The `--check --json` outcome. stdout carries exactly one single-line JSON
384
- * document in EVERY case — including the resolution-failure family, which
385
- * keeps its non-zero exit but must never leave a bash gate with exit 1 and an
386
- * empty stdout. The human failure label stays on stderr; the in-sync/drift
387
- * text is superseded by the document.
439
+ * document (`json`, written with `Console.log`) in EVERY case — including the
440
+ * resolution-failure family, which keeps its non-zero exit but must never
441
+ * leave a bash gate with exit 1 and an empty stdout. A resolution failure is
442
+ * also returned as `failure`, which the command fails with so the labelled
443
+ * human report lands on stderr.
388
444
  *
389
445
  * @internal
390
446
  */
391
447
  function checkJsonOutcome(result) {
392
448
  if (Result.isFailure(result)) {
393
- const message = result.failure.message;
449
+ const { message, kind } = result.failure;
394
450
  return {
395
451
  exitCode: 1,
396
- stdout: jsonLine({
452
+ json: JSON.stringify({
397
453
  command: "check",
398
454
  inSync: false,
399
455
  error: {
400
- kind: result.failure.kind ?? "resolution",
456
+ kind: kind ?? "resolution",
401
457
  message
402
458
  }
403
459
  }),
404
- stderr: checkFailureOutcome(message).text
460
+ failure: new CheckFailedError({
461
+ message,
462
+ ...kind !== void 0 ? { kind } : {}
463
+ })
405
464
  };
406
465
  }
407
466
  const changed = result.success.changed;
408
467
  return {
409
468
  exitCode: changed.length === 0 ? 0 : 1,
410
- stdout: jsonLine({
469
+ json: JSON.stringify({
411
470
  command: "check",
412
471
  inSync: changed.length === 0,
413
472
  drift: changed.map(driftRowJson)
414
- }),
415
- stderr: ""
473
+ })
416
474
  };
417
475
  }
418
476
  /**
@@ -420,62 +478,59 @@ function checkJsonOutcome(result) {
420
478
  * the run wrote at least one change — always false under dry-run AND on an
421
479
  * already-in-sync run, so a consumer keying on it never reads a no-op as a
422
480
  * real apply; `changed` uses the same row object as check's `drift`. A failure
423
- * emits an error document on stdout with a non-zero exit and the human message
424
- * on stderr; `error.kind` is `"peer-strategy"` for a peer-strategy refusal and
425
- * `"resolution"` otherwise.
481
+ * emits an error document on stdout and is returned as `failure`, which the
482
+ * command fails with (non-zero exit, human message on stderr); `error.kind` is
483
+ * `"peer-strategy"` for a peer-strategy refusal and `"resolution"` otherwise.
426
484
  *
427
485
  * @internal
428
486
  */
429
487
  function upgradeJsonOutcome(result, dryRun) {
430
488
  if (Result.isFailure(result)) {
431
- const message = result.failure.message;
489
+ const { message, kind } = result.failure;
432
490
  return {
433
- exitCode: 1,
434
- stdout: jsonLine({
491
+ json: JSON.stringify({
435
492
  command: "upgrade",
436
493
  applied: false,
437
494
  error: {
438
- kind: result.failure.kind ?? "resolution",
495
+ kind: kind ?? "resolution",
439
496
  message
440
497
  }
441
498
  }),
442
- stderr: `${message}\n`
499
+ failure: result.failure
443
500
  };
444
501
  }
445
502
  const r = result.success;
446
- return {
447
- exitCode: 0,
448
- stdout: jsonLine({
449
- command: "upgrade",
450
- applied: !dryRun && r.updated > 0,
451
- updated: r.updated,
452
- changed: r.changed.map(driftRowJson),
453
- skipped: r.skipped,
454
- conflicts: r.conflicts.map((c) => ({
455
- pkg: c.pkg,
456
- ceiling: c.ceiling,
457
- blockedBy: c.blockedBy
458
- }))
459
- }),
460
- stderr: ""
461
- };
503
+ return { json: JSON.stringify({
504
+ command: "upgrade",
505
+ applied: !dryRun && r.updated > 0,
506
+ updated: r.updated,
507
+ changed: r.changed.map(driftRowJson),
508
+ skipped: r.skipped,
509
+ conflicts: r.conflicts.map((c) => ({
510
+ pkg: c.pkg,
511
+ ceiling: c.ceiling,
512
+ blockedBy: c.blockedBy
513
+ }))
514
+ }) };
462
515
  }
463
516
  /**
464
517
  * Reject `--json` outside the non-interactive modes it exists for. JSON mode
465
518
  * is a machine contract (a GitHub Action parsing stdout from bash); the
466
519
  * interactive table and the preview views have no meaningful document to emit.
467
- * Returns the rejection to fail with, or null when the combination is valid.
520
+ * Returns the usage error to fail with (exit 64), or null when the combination
521
+ * is valid.
468
522
  *
469
523
  * @internal
470
524
  */
471
525
  function validateJsonMode(flags) {
472
526
  if (!flags.json) return null;
473
- if (flags.preview) return new UpgradeError({ message: "--json cannot be combined with --preview; use --check, --yes, or --dry-run" });
474
- if (!flags.check && !flags.yes && !flags.dryRun) return new UpgradeError({ message: "--json requires a non-interactive mode: combine it with --check, --yes, or --dry-run" });
527
+ if (flags.preview) return new UpgradeUsageError({ message: "--json cannot be combined with --preview; use --check, --yes, or --dry-run" });
528
+ if (!flags.check && !flags.yes && !flags.dryRun) return new UpgradeUsageError({ message: "--json requires a non-interactive mode: combine it with --check, --yes, or --dry-run" });
475
529
  return null;
476
530
  }
477
531
  /**
478
- * Resolve the target file: the passed path, or autodetect in cwd.
532
+ * Resolve the target file: the passed path, or autodetect in the working
533
+ * directory.
479
534
  *
480
535
  * @internal
481
536
  */
@@ -483,20 +538,77 @@ function resolveTargetFile(fileOpt) {
483
538
  return Effect.gen(function* () {
484
539
  const explicit = Option.getOrUndefined(fileOpt);
485
540
  if (explicit !== void 0) return explicit;
486
- const matches = yield* findConfigFiles(process.cwd());
541
+ const matches = yield* findConfigFiles(yield* WorkingDirectory);
487
542
  const picked = pickConfigCandidate(matches);
488
543
  if (!picked.ok) return yield* Effect.fail(new UpgradeError({ message: picked.message }));
489
544
  return picked.file;
490
545
  });
491
546
  }
492
- const fileArg = Argument.File("file", { mustExist: true }).pipe(Argument.optional);
493
- const yesFlag = Flag.Boolean("yes").pipe(Flag.withAlias("y"), Flag.withDefault(false));
494
- const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDefault(false));
495
- const catalogOption = Flag.String("catalog").pipe(Flag.optional);
496
- const previewFlag = Flag.Boolean("preview").pipe(Flag.withDefault(false));
497
- const fullFlag = Flag.Boolean("full").pipe(Flag.withDefault(false));
498
- const checkFlag = Flag.Boolean("check").pipe(Flag.withDefault(false));
499
- const jsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false));
547
+ /** Print the unresolved-packages warning to stderr, when there is one. */
548
+ function warnUnresolved(unresolved) {
549
+ return unresolved.length > 0 ? printDoc(warningDoc(unresolvedMessage(unresolved)), { stream: "stderr" }) : Effect.void;
550
+ }
551
+ /**
552
+ * Resolve every entry's versions, then — for interop catalogs — pre-fetch the
553
+ * peerDependencies of each candidate version so the live table can recompute
554
+ * peer floors + conflicts as picks change (the same data `runInterop` fetches,
555
+ * moved ahead of the walk). Interactively the live table IS the reconcile;
556
+ * `--yes`/CI keeps the auto-reconcile (`runUpgrade`). Each phase reports
557
+ * progress, which an interactive run draws as a live view.
558
+ */
559
+ function resolveForWalk(entries, resolver, gate, workspaceResolver, report) {
560
+ return Effect.gen(function* () {
561
+ const versions = yield* resolveGatedVersions(entries, resolver, gate, Date.now(), resolveProgress(report), workspaceResolver);
562
+ const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
563
+ const interopByCatalog = /* @__PURE__ */ new Map();
564
+ for (const e of entries) {
565
+ if (e.strategy !== "interop") continue;
566
+ const list = interopByCatalog.get(e.catalog) ?? [];
567
+ list.push(e);
568
+ interopByCatalog.set(e.catalog, list);
569
+ }
570
+ const interopModels = /* @__PURE__ */ new Map();
571
+ if (interopByCatalog.size > 0) {
572
+ const fetchPeer = makePeerFetcher(resolver);
573
+ const total = interopByCatalog.size;
574
+ yield* report({
575
+ _tag: "Phase",
576
+ label: "Resolving peer dependencies",
577
+ total
578
+ });
579
+ for (const [catalog, group] of interopByCatalog) {
580
+ const candByPkg = /* @__PURE__ */ new Map();
581
+ for (const e of group) {
582
+ const it = items.find((i) => i.entry.catalog === catalog && i.entry.pkg === e.pkg);
583
+ candByPkg.set(e.pkg, it ? it.candidates.map((c) => c.version) : [bareVersion(e.currentRange)]);
584
+ }
585
+ interopModels.set(catalog, yield* buildGroupModel(candByPkg, fetchPeer));
586
+ yield* report({
587
+ _tag: "Step",
588
+ done: interopModels.size
589
+ });
590
+ }
591
+ yield* report({
592
+ _tag: "Finished",
593
+ label: "Resolved peer dependencies"
594
+ });
595
+ }
596
+ return {
597
+ versions,
598
+ items,
599
+ interopByCatalog,
600
+ interopModels
601
+ };
602
+ });
603
+ }
604
+ const fileArg = Argument.File("file").pipe(Argument.withDescription("The config file to upgrade (autodetected in the current directory when omitted)"), Argument.optional);
605
+ const yesFlag = Flag.Boolean("yes").pipe(Flag.withAlias("y"), Flag.withDescription("Apply the latest in-range version of every entry without asking (never a major)"), Flag.withDefault(false));
606
+ const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDescription("Run the whole flow and report what would change, but write nothing"), Flag.withDefault(false));
607
+ const catalogOption = Flag.String("catalog").pipe(Flag.withDescription("Only upgrade entries in the named catalog"), Flag.optional);
608
+ const previewFlag = Flag.Boolean("preview").pipe(Flag.withDescription("Print the default picks as a summary and exit"), Flag.withDefault(false));
609
+ const fullFlag = Flag.Boolean("full").pipe(Flag.withDescription("Include up-to-date packages in a printed summary"), Flag.withDefault(false));
610
+ const checkFlag = Flag.Boolean("check").pipe(Flag.withDescription("Exit 1 when --yes would change anything (a drift gate; never writes)"), Flag.withDefault(false));
611
+ const jsonFlag = Flag.Boolean("json").pipe(Flag.withDescription("Print one JSON document on stdout (with --check, --yes or --dry-run)"), Flag.withDefault(false));
500
612
  /**
501
613
  * The "upgrade" command. The default path runs the interactive table;
502
614
  * --yes applies latest-in-range non-interactively; --dry-run runs the identical
@@ -523,111 +635,91 @@ const upgradeCommand = Command.make("upgrade", {
523
635
  preview
524
636
  });
525
637
  if (jsonRejection !== null) return yield* Effect.fail(jsonRejection);
526
- const file = yield* resolveTargetFile(fileOpt);
527
638
  const resolver = yield* RegistryResolver;
528
- const caps = detectCapabilities();
529
- const workspaceResolver = makeWorkspaceResolver(findWorkspaceRoot(dirname(file)));
639
+ const workspaceOf = (file) => makeWorkspaceResolver(findWorkspaceRoot(dirname(file)));
640
+ const nonInteractive = (forceDryRun) => resolveTargetFile(fileOpt).pipe(Effect.flatMap((file) => runUpgrade({
641
+ file,
642
+ resolver,
643
+ workspaceResolver: workspaceOf(file),
644
+ dryRun: forceDryRun || dryRun
645
+ })), Effect.result);
530
646
  if (check) {
531
- const result = yield* runUpgrade({
532
- file,
533
- resolver,
534
- workspaceResolver,
535
- dryRun: true
536
- }).pipe(Effect.result);
647
+ const result = yield* nonInteractive(true);
537
648
  if (json) {
538
649
  const o = checkJsonOutcome(result);
539
- yield* Effect.sync(() => {
540
- if (o.stderr !== "") process.stderr.write(o.stderr);
541
- process.stdout.write(o.stdout);
542
- process.exitCode = o.exitCode;
543
- });
544
- return;
650
+ yield* Console.log(o.json);
651
+ if (o.failure !== void 0) return yield* Effect.fail(o.failure);
652
+ return yield* CliExit.set(o.exitCode);
545
653
  }
546
- const failed = Result.isFailure(result);
547
- const outcome = failed ? checkFailureOutcome(result.failure.message) : checkOutcome(result.success.changed);
548
- yield* Effect.sync(() => {
549
- (failed ? process.stderr : process.stdout).write(outcome.text);
550
- process.exitCode = outcome.exitCode;
551
- });
552
- return;
654
+ if (Result.isFailure(result)) {
655
+ const { message, kind } = result.failure;
656
+ return yield* Effect.fail(new CheckFailedError({
657
+ message,
658
+ ...kind !== void 0 ? { kind } : {}
659
+ }));
660
+ }
661
+ const outcome = checkOutcome(result.success.changed);
662
+ yield* printDoc(outcome.doc);
663
+ return yield* CliExit.set(outcome.exitCode);
553
664
  }
554
665
  if (json) {
555
- const o = upgradeJsonOutcome(yield* runUpgrade({
556
- file,
557
- resolver,
558
- dryRun,
559
- workspaceResolver
560
- }).pipe(Effect.result), dryRun);
561
- yield* Effect.sync(() => {
562
- if (o.stderr !== "") process.stderr.write(o.stderr);
563
- process.stdout.write(o.stdout);
564
- process.exitCode = o.exitCode;
565
- });
666
+ const o = upgradeJsonOutcome(yield* nonInteractive(false), dryRun);
667
+ yield* Console.log(o.json);
668
+ if (o.failure !== void 0) return yield* Effect.fail(o.failure);
566
669
  return;
567
670
  }
671
+ const file = yield* resolveTargetFile(fileOpt);
672
+ const workspaceResolver = workspaceOf(file);
568
673
  if (preview) {
569
- const text = yield* runUpgradePreview({
674
+ const { doc, unresolved } = yield* runUpgradePreview({
570
675
  file,
571
676
  resolver,
572
677
  full,
573
- color: caps.color,
574
678
  workspaceResolver
575
679
  });
576
- yield* Effect.sync(() => process.stdout.write(`${text}\n`));
577
- return;
680
+ yield* printDoc(doc);
681
+ return yield* warnUnresolved(unresolved);
578
682
  }
579
683
  if (yes) {
580
- const result = yield* runUpgrade({
684
+ const result = yield* withProgress((report) => runUpgrade({
581
685
  file,
582
686
  resolver,
583
687
  dryRun,
584
688
  workspaceResolver,
585
- ...caps.interactive ? { onProgress: writeResolveProgress } : {}
586
- });
587
- yield* Effect.sync(() => process.stdout.write(dryRun ? `Dry run — no changes written. ${result.updated} package(s) would be updated; skipped ${result.skipped.length}.\n` : `Updated ${result.updated} package(s); skipped ${result.skipped.length}.\n`));
689
+ onProgress: resolveProgress(report)
690
+ }));
691
+ yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${result.updated} package(s) would be updated; skipped ${result.skipped.length}.`) : CliMessage.success(`Updated ${result.updated} package(s); skipped ${result.skipped.length}.`);
588
692
  if (result.conflicts.length > 0) {
589
- const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`).join("\n");
590
- yield* Effect.sync(() => process.stdout.write(`Interop conflicts (left at your pick):\n${lines}\n`));
693
+ const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`);
694
+ yield* printDoc(warningDoc(["Interop conflicts (left at your pick):", ...lines].join("\n")), { stream: "stderr" });
591
695
  }
592
696
  return;
593
697
  }
594
698
  const { source, entries: discovered } = yield* readCatalogSource(file);
595
- const catalogName = Option.getOrUndefined(catalog);
596
- const entries = filterEntriesByCatalog(discovered, catalogName);
597
- const versions = yield* resolveGatedVersions(entries, resolver, yield* computeGate(source, file, resolver), Date.now(), caps.interactive ? writeResolveProgress : void 0, workspaceResolver);
598
- const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
599
- if (!caps.interactive) {
600
- const text = renderSummary(projectDecisions(items, full), void 0, { color: caps.color });
601
- const note = dryRun ? "(dry run — nothing written)" : "(non-interactive terminal — run with --yes to apply, or in a TTY to choose)";
602
- const warn = versions.unresolved.length > 0 ? `\n⚠ ${unresolvedMessage(versions.unresolved)}\n` : "";
603
- yield* Effect.sync(() => process.stdout.write(`${text}${warn}\n\n${note}\n`));
604
- return;
699
+ const entries = filterEntriesByCatalog(discovered, Option.getOrUndefined(catalog));
700
+ const gate = yield* computeGate(source, file, resolver);
701
+ if (!(yield* CliInteractive)) {
702
+ const versions = yield* resolveGatedVersions(entries, resolver, gate, Date.now(), void 0, workspaceResolver);
703
+ const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
704
+ const note = dryRun ? "(dry run — nothing written)" : "(not interactive — run with --yes to apply, or in a terminal to choose)";
705
+ yield* printDoc([
706
+ ...summaryDoc(projectDecisions(items, full)),
707
+ Doc.line(""),
708
+ Doc.line(Doc.text(note, "muted"))
709
+ ]);
710
+ return yield* warnUnresolved(versions.unresolved);
605
711
  }
712
+ const { versions, items, interopByCatalog, interopModels } = yield* withProgress((report) => resolveForWalk(entries, resolver, gate, workspaceResolver, report));
606
713
  if (items.length === 0) {
607
- const warn = versions.unresolved.length > 0 ? `⚠ ${unresolvedMessage(versions.unresolved)}\n\n` : "";
608
- yield* Effect.sync(() => process.stdout.write(`${warn}${nothingToUpgradeMessage(0)}`));
609
- return;
610
- }
611
- const interopByCatalog = /* @__PURE__ */ new Map();
612
- for (const e of entries) {
613
- if (e.strategy !== "interop") continue;
614
- const list = interopByCatalog.get(e.catalog) ?? [];
615
- list.push(e);
616
- interopByCatalog.set(e.catalog, list);
714
+ yield* warnUnresolved(versions.unresolved);
715
+ return yield* CliMessage.info(nothingToUpgradeMessage(0));
617
716
  }
618
- const fetchPeer = makePeerFetcher(resolver);
619
- if (interopByCatalog.size > 0) yield* Effect.sync(() => process.stderr.write("Resolving peer dependencies…\n"));
620
- const interopModels = /* @__PURE__ */ new Map();
621
- for (const [catalog, group] of interopByCatalog) {
622
- const candByPkg = /* @__PURE__ */ new Map();
623
- for (const e of group) {
624
- const it = items.find((i) => i.entry.catalog === catalog && i.entry.pkg === e.pkg);
625
- candByPkg.set(e.pkg, it ? it.candidates.map((c) => c.version) : [bareVersion(e.currentRange)]);
626
- }
627
- interopModels.set(catalog, yield* buildGroupModel(candByPkg, fetchPeer));
628
- }
629
- const { runWalk } = yield* Effect.promise(() => import("../ui/run-walk.js"));
630
- const decisions = yield* runWalk(items, dryRun, versions.unresolved, interopModels);
717
+ const decisions = yield* CliUi.run(walkScreen({
718
+ items,
719
+ dryRun,
720
+ unresolved: versions.unresolved,
721
+ interopModels
722
+ }));
631
723
  const nonInteropDecisions = decisions.filter((d) => d.item.entry.strategy !== "interop");
632
724
  const interopEdits = [];
633
725
  const allConflicts = [];
@@ -663,12 +755,12 @@ const upgradeCommand = Command.make("upgrade", {
663
755
  const planned = buildEdits(nonInteropDecisions);
664
756
  const { accepted, rejected } = yield* validateEdits(planned, versions.raw);
665
757
  const acceptedPkgs = new Set(accepted.map((e) => e.pkg));
666
- yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, { conflicts: allConflicts }, { color: caps.color }, rejected)}\n`));
758
+ yield* printDoc(summaryDoc(decisions, { conflicts: allConflicts }, rejected));
667
759
  if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
668
760
  const changed = countChangedDecisions(nonInteropDecisions.filter((d) => acceptedPkgs.has(d.item.entry.pkg))) + interopChanged;
669
- yield* Effect.sync(() => process.stdout.write(dryRun ? `Dry run — no changes written. ${changed} change(s) would be applied.\n` : `Applied ${changed} change(s).\n`));
670
- if (versions.unresolved.length > 0) yield* Effect.sync(() => process.stdout.write(`\n⚠ ${unresolvedMessage(versions.unresolved)}\n`));
671
- }).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeServices.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
761
+ yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${changed} change(s) would be applied.`) : CliMessage.success(`Applied ${changed} change(s).`);
762
+ yield* warnUnresolved(versions.unresolved);
763
+ }).pipe(Effect.provide(RegistryResolverLive))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
672
764
 
673
765
  //#endregion
674
- export { UpgradeError, applyInteropAndDecisions, checkFailureOutcome, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateJsonMode, writeResolveProgress };
766
+ export { CHECK_FAILED_LABEL, CheckFailedError, UpgradeError, UpgradeUsageError, applyInteropAndDecisions, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveProgress, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateJsonMode };