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.
package/cli/cwd.js ADDED
@@ -0,0 +1,25 @@
1
+ import { findWorkspaceFile } from "./workspace-file.js";
2
+ import { Context, Effect } from "effect";
3
+ import { join } from "node:path";
4
+
5
+ //#region src/cli/cwd.ts
6
+ /**
7
+ * The directory the CLI was invoked from. `main.ts` reads `process.cwd()` once
8
+ * and provides it here, so no command reads `process` itself.
9
+ *
10
+ * @internal
11
+ */
12
+ var WorkingDirectory = class extends Context.Service()("rolldown-pnpm-config/WorkingDirectory") {};
13
+ /**
14
+ * The `pnpm-workspace.yaml` a command targets: the explicit path when given,
15
+ * else the nearest one upward from the working directory, else the working
16
+ * directory's own (to be created).
17
+ *
18
+ * @internal
19
+ */
20
+ function resolveWorkspacePath(explicit) {
21
+ return Effect.map(WorkingDirectory, (cwd) => explicit ?? findWorkspaceFile(cwd) ?? join(cwd, "pnpm-workspace.yaml"));
22
+ }
23
+
24
+ //#endregion
25
+ export { WorkingDirectory, resolveWorkspacePath };
@@ -1,3 +1,6 @@
1
+ import { row, tone } from "../render/tone.js";
2
+ import { Doc } from "@effected/cli";
3
+
1
4
  //#region src/cli/diff/render.ts
2
5
  const CONTEXT = 2;
3
6
  const GUTTER = {
@@ -6,7 +9,7 @@ const GUTTER = {
6
9
  changed: "~",
7
10
  unchanged: " "
8
11
  };
9
- const STYLE = {
12
+ const TONE = {
10
13
  added: "added",
11
14
  removed: "removed",
12
15
  changed: "changed",
@@ -22,18 +25,11 @@ function flatten(node, depth, inheritedUnmanaged = false) {
22
25
  const tag = node.tag;
23
26
  const changed = node.kind !== "unchanged";
24
27
  const unmanaged = inheritedUnmanaged || node.tag === "unmanaged" && node.kind === "unchanged";
25
- const style = node.kind === "unchanged" && unmanaged ? "unmanaged" : STYLE[node.kind];
28
+ const t = node.kind === "unchanged" && unmanaged ? "unmanaged" : TONE[node.kind];
26
29
  const isArrayEl = node.arrayElement === true;
27
30
  if (node.children) return [{
28
- line: {
29
- indent,
30
- gutter,
31
- segments: [{
32
- text: `${node.key}:`,
33
- style
34
- }],
35
- ...tag ? { tag } : {}
36
- },
31
+ row: row(gutter, indent, [tone(`${node.key}:`, t)], tag),
32
+ indent,
37
33
  changed
38
34
  }, ...node.children.flatMap((c) => flatten(c, depth + 1, unmanaged))];
39
35
  let text;
@@ -41,28 +37,21 @@ function flatten(node, depth, inheritedUnmanaged = false) {
41
37
  else if (isArrayEl) text = `- ${node.key}`;
42
38
  else text = `${node.key}: ${scalarText(node.after ?? node.before)}`;
43
39
  return [{
44
- line: {
45
- indent,
46
- gutter,
47
- segments: [{
48
- text,
49
- style
50
- }],
51
- ...tag ? { tag } : {}
52
- },
40
+ row: row(gutter, indent, [tone(text, t)], tag),
41
+ indent,
53
42
  changed
54
43
  }];
55
44
  }
56
45
  /**
57
- * Render a diff tree to styled lines in canonical-YAML shape. Default collapses
58
- * unchanged lines outside a 2-line window around changes into a single
59
- * "… N unchanged" marker; `full` keeps every line.
46
+ * Render a diff tree to a `Lines` block in canonical-YAML shape. Default
47
+ * collapses unchanged lines outside a 2-line window around changes into a
48
+ * single "… N unchanged" marker; `full` keeps every line.
60
49
  *
61
50
  * @internal
62
51
  */
63
52
  function renderExportDiff(root, opts) {
64
53
  const flats = (root.children ?? []).flatMap((c) => flatten(c, 0));
65
- if (opts.full) return flats.map((f) => f.line);
54
+ if (opts.full) return Doc.lines(flats.map((f) => f.row));
66
55
  const keep = new Array(flats.length).fill(false);
67
56
  flats.forEach((f, i) => {
68
57
  if (!f.changed) return;
@@ -70,35 +59,28 @@ function renderExportDiff(root, opts) {
70
59
  });
71
60
  for (let i = 0; i < flats.length; i++) {
72
61
  if (!keep[i]) continue;
73
- let depth = flats[i].line.indent;
74
- for (let j = i - 1; j >= 0 && depth > 0; j--) if (flats[j].line.indent < depth) {
62
+ let depth = flats[i].indent;
63
+ for (let j = i - 1; j >= 0 && depth > 0; j--) if (flats[j].indent < depth) {
75
64
  keep[j] = true;
76
- depth = flats[j].line.indent;
65
+ depth = flats[j].indent;
77
66
  }
78
67
  }
79
68
  const out = [];
80
69
  let dropped = 0;
81
70
  const flushDropped = () => {
82
71
  if (dropped > 0) {
83
- out.push({
84
- indent: 0,
85
- gutter: " ",
86
- segments: [{
87
- text: `… ${dropped} unchanged`,
88
- style: "unchanged"
89
- }]
90
- });
72
+ out.push(row(" ", 0, [tone(`… ${dropped} unchanged`, "unchanged")]));
91
73
  dropped = 0;
92
74
  }
93
75
  };
94
76
  flats.forEach((f, i) => {
95
77
  if (keep[i]) {
96
78
  flushDropped();
97
- out.push(f.line);
79
+ out.push(f.row);
98
80
  } else dropped++;
99
81
  });
100
82
  flushDropped();
101
- return out;
83
+ return Doc.lines(out);
102
84
  }
103
85
 
104
86
  //#endregion
@@ -1,14 +1,13 @@
1
+ import { parseWorkspace } from "./workspace-file.js";
1
2
  import { evaluatePluginConfig } from "./evaluate.js";
2
- import { findWorkspaceFile, parseWorkspace } from "./workspace-file.js";
3
3
  import { Effect, Predicate } from "effect";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
5
 
7
6
  //#region src/cli/load-config.ts
8
7
  /**
9
8
  * The prologue shared by `export` and `preview`: read and statically evaluate
10
- * the config file (failing on a missing call or non-literal values), locate
11
- * the workspace file, and parse it when present. `mkError` wraps each failure
9
+ * the config file (failing on a missing call or non-literal values), and
10
+ * parse the workspace file when present. `mkError` wraps each failure
12
11
  * message in the command's own error type.
13
12
  *
14
13
  * @internal
@@ -22,7 +21,7 @@ function loadConfigAndWorkspace(opts, mkError) {
22
21
  const { config, errors } = evaluatePluginConfig(configSource, opts.configFile);
23
22
  if (config === null) return yield* Effect.fail(mkError(`No PnpmConfigPlugin call found in ${opts.configFile}`));
24
23
  if (errors.length > 0) return yield* Effect.fail(mkError(`Non-literal config values: ${errors.join("; ")}`));
25
- const path = opts.workspacePath ?? findWorkspaceFile(process.cwd()) ?? join(process.cwd(), "pnpm-workspace.yaml");
24
+ const path = opts.workspacePath;
26
25
  const parsed = existsSync(path) ? yield* Effect.try({
27
26
  try: () => parseWorkspace(readFileSync(path, "utf8")),
28
27
  catch: (e) => mkError(`Cannot read or parse ${path}: ${String(e)}`)
package/cli/main.js ADDED
@@ -0,0 +1,37 @@
1
+ import { WorkingDirectory } from "./cwd.js";
2
+ import { rootCommand } from "./root.js";
3
+ import { CLI_VERSION } from "./version.js";
4
+ import { Effect } from "effect";
5
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
6
+ import { CliAudience, CliRuntime } from "@effected/cli";
7
+
8
+ //#region src/cli/main.ts
9
+ /**
10
+ * Assemble and run the CLI. One of the three files allowed to read `process`:
11
+ * the working directory, argv for the build-time log format, and stderr's
12
+ * terminal state are read here and passed down as plain values.
13
+ *
14
+ * `CliRuntime.main` builds the audience, terminal and theme from `env`,
15
+ * decides whether the run may prompt, reports every failure on stderr for the
16
+ * run's audience, and maps the exit code (usage 64, cancel 130, otherwise 1).
17
+ *
18
+ * @internal
19
+ */
20
+ function main() {
21
+ const program = CliAudience.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(WorkingDirectory, process.cwd()));
22
+ NodeRuntime.runMain(CliRuntime.main(program, {
23
+ platform: NodeServices.layer,
24
+ helpOnUsageError: "stderr",
25
+ env: {
26
+ audienceEnvVar: "ROLLDOWN_PNPM_CONFIG_AUDIENCE",
27
+ log: {
28
+ envVar: "ROLLDOWN_PNPM_CONFIG_LOG_LEVEL",
29
+ argv: process.argv.slice(2)
30
+ },
31
+ stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true)
32
+ }
33
+ }));
34
+ }
35
+
36
+ //#endregion
37
+ export { main };
@@ -1,14 +1,14 @@
1
+ import { canonicalize } from "./workspace-file.js";
1
2
  import { buildDiff } from "./diff/build.js";
2
3
  import { renderExportDiff } from "./diff/render.js";
3
4
  import { effectiveManaged, vanillaManaged } from "./effective.js";
4
- import { canonicalize } from "./workspace-file.js";
5
5
  import { overlayWorkspace } from "./workspace-overlay.js";
6
6
  import { WORKSPACE_FIELDS } from "./commands/export.js";
7
7
  import { renderSimulated } from "./simulated-view.js";
8
8
 
9
9
  //#region src/cli/preview-views.ts
10
10
  /**
11
- * Build the three preview views as styled lines: Changes (parsed→merged diff,
11
+ * Build the three preview views as `Doc` blocks: Changes (parsed→merged diff,
12
12
  * with local + preserve + excludeByRepo), Full (same tree, full verbosity), and
13
13
  * Simulated (the fresh-consumer output rendered as a plain calculated file — not
14
14
  * a diff — with per-field merge/overwrite + enforcement annotations).
@@ -0,0 +1,42 @@
1
+ import { tone } from "./tone.js";
2
+ import { Doc } from "@effected/cli";
3
+
4
+ //#region src/cli/render/legend.ts
5
+ function swatches(entries) {
6
+ return Doc.line([Doc.text(" Legend: "), ...entries.map(([label, t], i) => tone(`■ ${label}${i < entries.length - 1 ? " " : ""}`, t))]);
7
+ }
8
+ /**
9
+ * The colour legend for the config diff (the `preview` command and
10
+ * `export --dry-run`). Each swatch carries the matching tone, so the legend
11
+ * tracks the palette. Printed only when the output is coloured — without
12
+ * colour the swatches are indistinguishable and the legend is noise.
13
+ *
14
+ * @internal
15
+ */
16
+ function legend() {
17
+ return swatches([
18
+ ["added", "added"],
19
+ ["removed", "removed"],
20
+ ["modified", "changed"],
21
+ ["unchanged", "unchanged"],
22
+ ["unmanaged", "unmanaged"]
23
+ ]);
24
+ }
25
+ /**
26
+ * The legend for the Simulated view. That view is not a diff — nothing is
27
+ * added or removed — so it has its own vocabulary: how each field is combined
28
+ * (`merge`/`overwrite`) and how divergence is enforced (`warn`/`error`).
29
+ *
30
+ * @internal
31
+ */
32
+ function simulatedLegend() {
33
+ return swatches([
34
+ ["merge", "merge"],
35
+ ["overwrite", "overwrite"],
36
+ ["warn", "changed"],
37
+ ["error", "warn"]
38
+ ]);
39
+ }
40
+
41
+ //#endregion
42
+ export { legend, simulatedLegend };
@@ -0,0 +1,50 @@
1
+ import { Effect } from "effect";
2
+ import { CliTheme, Doc } from "@effected/cli";
3
+ import { Audience } from "@effected/env";
4
+
5
+ //#region src/cli/render/print.ts
6
+ /**
7
+ * Whether stdout is painted for a person: a human audience at a colour level
8
+ * above `none`. A legend is only worth printing then — without colour its
9
+ * swatches are indistinguishable.
10
+ *
11
+ * @internal
12
+ */
13
+ const coloured = Effect.gen(function* () {
14
+ const theme = yield* CliTheme;
15
+ return (yield* Audience).kind === "human" && theme.color !== "none";
16
+ });
17
+ /**
18
+ * Print a document with no width limit, for every audience. Diffs, summaries
19
+ * and package lists are line-oriented data: a hard wrap at a person's width (80
20
+ * when output is piped) would split a row away from its gutter and break a
21
+ * grep. A terminal still soft-wraps a long line when it shows it.
22
+ *
23
+ * @internal
24
+ */
25
+ function printDoc(doc, options = {}) {
26
+ return Doc.print(doc, {
27
+ ...options,
28
+ width: Number.POSITIVE_INFINITY
29
+ });
30
+ }
31
+ /**
32
+ * Print a diff-shaped view to stdout: an optional heading line, the legend
33
+ * when the output is coloured, then the view. The audience picks the
34
+ * renderer (ANSI for a person, plain for an agent, the GitHub log under
35
+ * Actions).
36
+ *
37
+ * @internal
38
+ */
39
+ function printView(view, options = {}) {
40
+ return Effect.gen(function* () {
41
+ const doc = [];
42
+ if (options.heading !== void 0) doc.push(Doc.line(options.heading), Doc.line(""));
43
+ if (options.legend !== void 0 && (yield* coloured)) doc.push(options.legend, Doc.line(""));
44
+ doc.push(view);
45
+ yield* printDoc(doc);
46
+ });
47
+ }
48
+
49
+ //#endregion
50
+ export { coloured, printDoc, printView };
@@ -0,0 +1,38 @@
1
+ import { Doc, Status } from "@effected/cli";
2
+
3
+ //#region src/cli/render/report.ts
4
+ function statusDoc(status, message) {
5
+ const [first = "", ...rest] = message.split("\n");
6
+ const blocks = [Doc.line([
7
+ Doc.status(Status.core, status),
8
+ " ",
9
+ first
10
+ ])];
11
+ if (rest.length > 0) blocks.push(Doc.verbatim(rest.join("\n")));
12
+ return blocks;
13
+ }
14
+ /**
15
+ * The failure report for one of the CLI's own errors: a status line carrying
16
+ * the message's first line, then any further lines as they were written (a
17
+ * list of packages, say). Used by each error's `CliDoc` method, so
18
+ * `CliRuntime.main` draws the message without the error class's name.
19
+ *
20
+ * @internal
21
+ */
22
+ function failureDoc(message) {
23
+ return statusDoc("failure", message);
24
+ }
25
+ /**
26
+ * A multi-line warning drawn like a failure report but with the warning mark:
27
+ * the first line beside the glyph, the rest as written. For a warning longer
28
+ * than the one line `CliMessage.warning` takes (a list of packages, say);
29
+ * printed to stderr.
30
+ *
31
+ * @internal
32
+ */
33
+ function warningDoc(message) {
34
+ return statusDoc("warning", message);
35
+ }
36
+
37
+ //#endregion
38
+ export { failureDoc, warningDoc };
@@ -0,0 +1,43 @@
1
+ import { Doc, Token } from "@effected/cli";
2
+
3
+ //#region src/cli/render/tone.ts
4
+ /**
5
+ * The palette: one theme token (or explicit style) per tone. Semantic tones
6
+ * use the theme's tokens so a theme override restyles them; `unmanaged` is a
7
+ * fixed dark gray (distinct from the dim `unchanged`) and `local`/`overwrite`
8
+ * are magenta. `plain` is unstyled.
9
+ *
10
+ * @internal
11
+ */
12
+ const TONES = {
13
+ added: "success",
14
+ removed: "failure",
15
+ changed: "warning",
16
+ unchanged: "muted",
17
+ warn: "failure",
18
+ local: Token.named("magenta"),
19
+ unmanaged: Token.hex("#585858"),
20
+ merge: "accent",
21
+ overwrite: Token.named("magenta")
22
+ };
23
+ /** A run of text in a tone. @internal */
24
+ function tone(text, t) {
25
+ return t === "plain" ? Doc.text(text) : Doc.text(text, TONES[t]);
26
+ }
27
+ /**
28
+ * One diff-shaped row: `<gutter><space><2-space indent><content><tag>`. The
29
+ * gutter and the tag are plain characters, so a row keeps its meaning when the
30
+ * renderer drops colour (an agent, a pipe, `NO_COLOR`).
31
+ *
32
+ * @internal
33
+ */
34
+ function row(gutter, indent, content, tag) {
35
+ return [
36
+ Doc.text(`${gutter} ${" ".repeat(indent)}`),
37
+ ...content,
38
+ ...tag === void 0 ? [] : [tone(` (${tag})`, tag)]
39
+ ];
40
+ }
41
+
42
+ //#endregion
43
+ export { TONES, row, tone };
package/cli/root.js ADDED
@@ -0,0 +1,22 @@
1
+ import { exportCommand } from "./commands/export.js";
2
+ import { previewCommand } from "./commands/preview.js";
3
+ import { upgradeCommand } from "./commands/upgrade.js";
4
+ import { CliAudience } from "@effected/cli";
5
+ import { Command } from "effect/cli";
6
+
7
+ //#region src/cli/root.ts
8
+ /**
9
+ * The `rolldown-pnpm-config` command tree, with the audience flags
10
+ * (`--audience`, `--human`, `--agent`, `--ci`) shared from the root so every
11
+ * subcommand accepts them.
12
+ *
13
+ * @internal
14
+ */
15
+ const rootCommand = Command.make("rolldown-pnpm-config").pipe(Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([
16
+ upgradeCommand,
17
+ exportCommand,
18
+ previewCommand
19
+ ]));
20
+
21
+ //#endregion
22
+ export { rootCommand };
@@ -1,5 +1,7 @@
1
- import { scalarText } from "./diff/render.js";
2
1
  import { canonicalize } from "./workspace-file.js";
2
+ import { row, tone } from "./render/tone.js";
3
+ import { scalarText } from "./diff/render.js";
4
+ import { Doc } from "@effected/cli";
3
5
 
4
6
  //#region src/cli/simulated-view.ts
5
7
  /**
@@ -22,14 +24,8 @@ const STRATEGY_VERB = {
22
24
  };
23
25
  /** The enforcement suffix, e.g. " · warn" / " · error"; empty when silent. */
24
26
  function enforcementSegs(e) {
25
- if (e === "warn") return [{
26
- text: " · warn",
27
- style: "changed"
28
- }];
29
- if (e === "error") return [{
30
- text: " · error",
31
- style: "warn"
32
- }];
27
+ if (e === "warn") return [tone(" · warn", "changed")];
28
+ if (e === "error") return [tone(" · error", "warn")];
33
29
  return [];
34
30
  }
35
31
  /** The trailing "(merge)" / "(overwrite · error)" annotation for one field. */
@@ -37,54 +33,17 @@ function annotation(entry) {
37
33
  if (!entry) return [];
38
34
  const verb = STRATEGY_VERB[entry.strategy] ?? "merge";
39
35
  return [
40
- {
41
- text: " (",
42
- style: "unchanged"
43
- },
44
- {
45
- text: verb,
46
- style: verb
47
- },
36
+ tone(" (", "unchanged"),
37
+ tone(verb, verb),
48
38
  ...enforcementSegs(entry.enforcement),
49
- {
50
- text: ")",
51
- style: "unchanged"
52
- }
39
+ tone(")", "unchanged")
53
40
  ];
54
41
  }
55
42
  /** Flatten one key/value into YAML-shaped plain lines; `ann` annotates the head. */
56
43
  function flatten(key, value, depth, ann) {
57
- if (Array.isArray(value)) return [{
58
- indent: depth,
59
- gutter: " ",
60
- segments: [{
61
- text: `${key}:`,
62
- style: "plain"
63
- }, ...ann]
64
- }, ...value.map((el) => ({
65
- indent: depth + 1,
66
- gutter: " ",
67
- segments: [{
68
- text: `- ${scalarText(el)}`,
69
- style: "plain"
70
- }]
71
- }))];
72
- if (value !== null && typeof value === "object") return [{
73
- indent: depth,
74
- gutter: " ",
75
- segments: [{
76
- text: `${key}:`,
77
- style: "plain"
78
- }, ...ann]
79
- }, ...Object.entries(value).flatMap(([k, v]) => flatten(k, v, depth + 1, []))];
80
- return [{
81
- indent: depth,
82
- gutter: " ",
83
- segments: [{
84
- text: `${key}: ${scalarText(value)}`,
85
- style: "plain"
86
- }, ...ann]
87
- }];
44
+ if (Array.isArray(value)) return [row(" ", depth, [tone(`${key}:`, "plain"), ...ann]), ...value.map((el) => row(" ", depth + 1, [tone(`- ${scalarText(el)}`, "plain")]))];
45
+ if (value !== null && typeof value === "object") return [row(" ", depth, [tone(`${key}:`, "plain"), ...ann]), ...Object.entries(value).flatMap(([k, v]) => flatten(k, v, depth + 1, []))];
46
+ return [row(" ", depth, [tone(`${key}: ${scalarText(value)}`, "plain"), ...ann])];
88
47
  }
89
48
  /**
90
49
  * Render the Simulated view: the calculated fresh-consumer config as a plain
@@ -96,7 +55,7 @@ function flatten(key, value, depth, ann) {
96
55
  */
97
56
  function renderSimulated(vanilla, manifest) {
98
57
  const canon = canonicalize(vanilla);
99
- return Object.entries(canon).flatMap(([k, v]) => flatten(k, v, 0, annotation(manifest[k])));
58
+ return Doc.lines(Object.entries(canon).flatMap(([k, v]) => flatten(k, v, 0, annotation(manifest[k]))));
100
59
  }
101
60
 
102
61
  //#endregion
package/cli/summary.js CHANGED
@@ -1,16 +1,17 @@
1
- import { toAnsi } from "./ui/ansi.js";
2
- import { displayCandidates, peerFor, tableLayout } from "./walk-reducer.js";
1
+ import { row, tone } from "./render/tone.js";
2
+ import { cellTone, displayCandidates, peerFor, tableLayout } from "./walk-reducer.js";
3
+ import { Doc } from "@effected/cli";
3
4
 
4
5
  //#region src/cli/summary.ts
5
6
  /**
6
- * Build the pending-decisions summary as styled lines: one table row per
7
+ * Build the pending-decisions summary as a document: one table row per
7
8
  * decision — mirroring the interactive selection table, catalog headers,
8
9
  * chosen bubble filled — then a dim tally, interop conflicts, and any
9
- * rejected edits. Pure; color is applied by `renderSummary`/`toAnsi`.
10
+ * rejected edits. Pure; the renderer the audience picks decides colour.
10
11
  *
11
12
  * @internal
12
13
  */
13
- function summaryLines(decisions, interop, rejected) {
14
+ function summaryDoc(decisions, interop, rejected) {
14
15
  const lines = [];
15
16
  let toUpdate = 0;
16
17
  let major = 0;
@@ -23,49 +24,18 @@ function summaryLines(decisions, interop, rejected) {
23
24
  const { entry } = item;
24
25
  if (entry.catalog !== lastCatalog) {
25
26
  lastCatalog = entry.catalog;
26
- lines.push({
27
- indent: 0,
28
- gutter: " ",
29
- segments: [{
30
- text: `── catalog: ${entry.catalog} ──`,
31
- style: "unchanged"
32
- }]
33
- });
27
+ lines.push(row(" ", 0, [tone(`── catalog: ${entry.catalog} ──`, "unchanged")]));
34
28
  }
35
29
  const cells = displayCandidates(item);
36
- const segments = [{
37
- text: entry.pkg.padEnd(pkgWidth + 2),
38
- style: "plain"
39
- }];
30
+ const content = [tone(entry.pkg.padEnd(pkgWidth + 2), "plain")];
40
31
  for (const c of cells) {
41
32
  const selected = c.kind === chosen.kind;
42
- const style = !selected ? "unchanged" : c.kind === "keep" ? "unchanged" : c.isMajor ? "changed" : "added";
43
- segments.push({
44
- text: cellText(c, selected),
45
- style
46
- });
33
+ content.push(tone(cellText(c, selected), cellTone(c, selected) ?? "unchanged"));
47
34
  }
48
- for (let ci = cells.length; ci < maxCells; ci++) segments.push({
49
- text: blankCell,
50
- style: "plain"
51
- });
52
- segments.push({
53
- text: `│ ${peerFor(item, chosen)}`,
54
- style: "unchanged"
55
- });
56
- lines.push({
57
- indent: 0,
58
- gutter: chosen.kind === "keep" ? " " : "~",
59
- segments
60
- });
61
- if (item.peerWarning) lines.push({
62
- indent: 1,
63
- gutter: "⚠",
64
- segments: [{
65
- text: item.peerWarning.message,
66
- style: "warn"
67
- }]
68
- });
35
+ for (let ci = cells.length; ci < maxCells; ci++) content.push(tone(blankCell, "plain"));
36
+ content.push(tone(`│ ${peerFor(item, chosen)}`, "unchanged"));
37
+ lines.push(row(chosen.kind === "keep" ? " " : "~", 0, content));
38
+ if (item.peerWarning) lines.push(row("⚠", 1, [tone(item.peerWarning.message, "warn")]));
69
39
  if (chosen.kind !== "keep") {
70
40
  toUpdate++;
71
41
  if (chosen.isMajor) major++;
@@ -74,59 +44,12 @@ function summaryLines(decisions, interop, rejected) {
74
44
  else if (!entry.peer && item.materializePeer) materialize++;
75
45
  else upToDate++;
76
46
  }
77
- lines.push({
78
- indent: 0,
79
- gutter: " ",
80
- segments: [{
81
- text: `${toUpdate} to update · ${major} major · ${resync} resync · ${materialize} new peer · ${upToDate} up to date`,
82
- style: "unchanged"
83
- }]
84
- });
85
- if (interop) for (const c of interop.conflicts) lines.push({
86
- indent: 0,
87
- gutter: "⚠",
88
- segments: [{
89
- text: `${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`,
90
- style: "warn"
91
- }]
92
- });
93
- if (rejected && rejected.length > 0) {
94
- lines.push({
95
- indent: 0,
96
- gutter: " ",
97
- segments: [{
98
- text: "",
99
- style: "plain"
100
- }]
101
- });
102
- lines.push({
103
- indent: 0,
104
- gutter: "⚠",
105
- segments: [{
106
- text: "Rejected (no published version satisfies these):",
107
- style: "warn"
108
- }]
109
- });
110
- for (const r of rejected) lines.push({
111
- indent: 1,
112
- gutter: "⚠",
113
- segments: [{
114
- text: `${r.pkg} ${r.kind} ${r.value} — ${r.reason}`,
115
- style: "warn"
116
- }]
117
- });
118
- }
119
- return lines;
120
- }
121
- /**
122
- * Render the summary to a string. Color defaults off so non-TTY/test callers
123
- * get clean text; the upgrade command passes the detected color flag.
124
- *
125
- * @internal
126
- */
127
- function renderSummary(decisions, interop, opts, rejected) {
128
- return toAnsi(summaryLines(decisions, interop, rejected), { color: opts?.color ?? false });
47
+ lines.push(row(" ", 0, [tone(`${toUpdate} to update · ${major} major · ${resync} resync · ${materialize} new peer · ${upToDate} up to date`, "unchanged")]));
48
+ if (interop) for (const c of interop.conflicts) lines.push(row("⚠", 0, [tone(`${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`, "warn")]));
49
+ const blocks = [Doc.lines(lines)];
50
+ if (rejected && rejected.length > 0) blocks.push(Doc.line(""), Doc.lines([row("⚠", 0, [tone("Rejected (no published version satisfies these):", "warn")]), ...rejected.map((r) => row("⚠", 1, [tone(`${r.pkg} ${r.kind} ${r.value} — ${r.reason}`, "warn")]))]));
51
+ return blocks;
129
52
  }
130
53
 
131
54
  //#endregion
132
- export { renderSummary, summaryLines };
55
+ export { summaryDoc };