rolldown-pnpm-config 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/bin/rolldown-pnpm-config.js +21 -0
  4. package/catalogs.js +27 -0
  5. package/cli/commands/export.js +118 -0
  6. package/cli/commands/preview.js +73 -0
  7. package/cli/commands/upgrade.js +437 -0
  8. package/cli/diff/build.js +122 -0
  9. package/cli/diff/render.js +103 -0
  10. package/cli/discover.js +128 -0
  11. package/cli/drift.js +22 -0
  12. package/cli/edits.js +41 -0
  13. package/cli/effective.js +42 -0
  14. package/cli/evaluate.js +91 -0
  15. package/cli/interop.js +285 -0
  16. package/cli/local-merge.js +75 -0
  17. package/cli/peer-range.js +34 -0
  18. package/cli/plan.js +66 -0
  19. package/cli/preview-views.js +34 -0
  20. package/cli/release-age.js +74 -0
  21. package/cli/resolve.js +109 -0
  22. package/cli/rewrite.js +22 -0
  23. package/cli/select-file.js +64 -0
  24. package/cli/summary.js +137 -0
  25. package/cli/ui/Preview.js +60 -0
  26. package/cli/ui/Walk.js +55 -0
  27. package/cli/ui/ansi.js +20 -0
  28. package/cli/ui/env.js +20 -0
  29. package/cli/ui/run-preview.js +23 -0
  30. package/cli/ui/run-walk.js +29 -0
  31. package/cli/ui/styled.js +27 -0
  32. package/cli/walk-plan.js +35 -0
  33. package/cli/walk-reducer.js +61 -0
  34. package/cli/workspace-file.js +58 -0
  35. package/cli/workspace-overlay.js +21 -0
  36. package/descriptors/build.js +248 -0
  37. package/descriptors/hoisting.js +175 -0
  38. package/descriptors/index.js +38 -0
  39. package/descriptors/lockfile.js +117 -0
  40. package/descriptors/misc.js +144 -0
  41. package/descriptors/network.js +108 -0
  42. package/descriptors/resolution.js +250 -0
  43. package/descriptors/runtime-cfg.js +90 -0
  44. package/descriptors/schemas.js +26 -0
  45. package/descriptors/workspace.js +116 -0
  46. package/index.d.ts +363 -0
  47. package/index.js +3 -0
  48. package/package.json +60 -0
  49. package/plugin/freeze.js +79 -0
  50. package/plugin/index.js +48 -0
  51. package/plugin/serialize.js +26 -0
  52. package/registry.js +8 -0
  53. package/runtime/ctx.js +39 -0
  54. package/runtime/enforcement.js +36 -0
  55. package/runtime/strategies/arrays.js +37 -0
  56. package/runtime/strategies/catalogs.js +36 -0
  57. package/runtime/strategies/maps.js +46 -0
  58. package/runtime/strategies/overrides.js +57 -0
  59. package/runtime/strategies/scalar.js +57 -0
  60. package/runtime/strategies/table.js +27 -0
  61. package/runtime/warnings.js +61 -0
  62. package/runtime.d.ts +111 -0
  63. package/runtime.js +54 -0
  64. package/tsdoc-metadata.json +11 -0
  65. package/virtual.d.ts +18 -0
@@ -0,0 +1,437 @@
1
+ import { evaluatePluginConfig } from "../evaluate.js";
2
+ import { discoverCatalogEntries } from "../discover.js";
3
+ import { filterEntriesByCatalog, findConfigFiles, pickConfigCandidate } from "../select-file.js";
4
+ import { detectCapabilities } from "../ui/env.js";
5
+ import { derivePeerRange } from "../peer-range.js";
6
+ import { detectPeerDrift } from "../drift.js";
7
+ import { buildEdits } from "../edits.js";
8
+ import { buildInteropEdits, capVersions, interopEntryChanged, reentryCandidates, runInterop } from "../interop.js";
9
+ import { planEntry } from "../plan.js";
10
+ import { combineReleaseAge, filterByReleaseAge, parsePnpmGate, readConfigReleaseAge } from "../release-age.js";
11
+ import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
12
+ import { applyEdits } from "../rewrite.js";
13
+ import { renderSummary } from "../summary.js";
14
+ import { runWalk } from "../ui/run-walk.js";
15
+ import { buildWalkItems } from "../walk-plan.js";
16
+ import { Data, Effect, Option } from "effect";
17
+ import { readFileSync, writeFileSync } from "node:fs";
18
+ import { Args, Command, Options } from "@effect/cli";
19
+ import { NodeContext } from "@effect/platform-node";
20
+
21
+ //#region src/cli/commands/upgrade.ts
22
+ /**
23
+ * Typed failure raised when the upgrade run cannot complete.
24
+ *
25
+ * @internal
26
+ */
27
+ var UpgradeError = class extends Data.TaggedError("UpgradeError") {};
28
+ /** Combine the config-declared and pnpm-resolved release-age gates (strictest of both). @internal */
29
+ function computeGate(source, file, resolver) {
30
+ return Effect.gen(function* () {
31
+ const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.catchAll(() => Effect.succeed({ config: null })));
32
+ const cfg = readConfigReleaseAge(config);
33
+ const [age, exc] = yield* Effect.all([resolver.pnpmConfig("minimumReleaseAge").pipe(Effect.catchAll(() => Effect.succeed(null))), resolver.pnpmConfig("minimumReleaseAgeExclude").pipe(Effect.catchAll(() => Effect.succeed(null)))], { concurrency: "unbounded" });
34
+ return combineReleaseAge(cfg, parsePnpmGate(age, exc));
35
+ });
36
+ }
37
+ /** Maximum number of per-package version+times fetches to issue concurrently. @internal */
38
+ const RESOLVE_CONCURRENCY = 12;
39
+ /**
40
+ * Fetch and age-gate the version list for each unique package.
41
+ *
42
+ * @param onProgress - Optional callback invoked after each package resolves with
43
+ * `(resolved, total)`. Useful for emitting CLI progress feedback. Called with
44
+ * `(0, total)` before any work starts so callers can emit the initial banner.
45
+ *
46
+ * @internal
47
+ */
48
+ function resolveGatedVersions(entries, resolver, gate, now, onProgress) {
49
+ const uniquePkgs = [...new Set(entries.map((e) => e.pkg))];
50
+ const total = uniquePkgs.length;
51
+ let resolved = 0;
52
+ onProgress?.(0, total);
53
+ return Effect.forEach(uniquePkgs, (pkg) => Effect.gen(function* () {
54
+ const vr = yield* resolver.versions(pkg).pipe(Effect.either);
55
+ if (vr._tag === "Left") {
56
+ onProgress?.(++resolved, total);
57
+ return [pkg, []];
58
+ }
59
+ const times = gate.ageMinutes > 0 ? yield* resolver.times(pkg).pipe(Effect.catchAll(() => Effect.succeed({}))) : {};
60
+ onProgress?.(++resolved, total);
61
+ return [pkg, filterByReleaseAge(vr.right, times, gate, pkg, now)];
62
+ }), { concurrency: 12 }).pipe(Effect.map((pairs) => new Map(pairs)));
63
+ }
64
+ /**
65
+ * Write a resolve-progress line to stderr. Overwrites the previous line with
66
+ * ANSI carriage return so the terminal shows a single updating counter instead
67
+ * of a flood of lines. The initial call (resolved === 0) writes a newline so
68
+ * the first subsequent overwrite lands on its own line.
69
+ *
70
+ * Only call when caps.interactive is true; this function is not gated itself.
71
+ *
72
+ * @internal
73
+ */
74
+ function writeResolveProgress(resolved, total) {
75
+ if (resolved === 0) process.stderr.write(`Resolving ${total} package${total === 1 ? "" : "s"}...\n`);
76
+ else if (resolved === total) process.stderr.write(`\r Resolved ${resolved}/${total} \n`);
77
+ else process.stderr.write(`\r Resolved ${resolved}/${total}`);
78
+ }
79
+ /**
80
+ * Non-interactive upgrade core: read the config, discover catalog entries,
81
+ * resolve + plan each, build edits for the latest-IN-RANGE candidate (and its
82
+ * recomputed peer literal), and write the file. Never selects a major bump.
83
+ *
84
+ * A package whose version list gates to empty (fetch failure / fully age-gated)
85
+ * is treated as a skip, except that a strategy entry can still resync or
86
+ * materialize its managed peer offline from the current range.
87
+ *
88
+ * @internal
89
+ */
90
+ function runUpgrade(opts) {
91
+ return Effect.gen(function* () {
92
+ const source = yield* Effect.try({
93
+ try: () => readFileSync(opts.file, "utf8"),
94
+ catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
95
+ });
96
+ const { entries, skipped } = yield* Effect.try({
97
+ try: () => discoverCatalogEntries(source, opts.file),
98
+ catch: (e) => new UpgradeError({ message: String(e) })
99
+ });
100
+ const gate = yield* computeGate(source, opts.file, opts.resolver);
101
+ const versionsByPkg = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), opts.onProgress);
102
+ const edits = [];
103
+ const changedSpans = /* @__PURE__ */ new Set();
104
+ for (const entry of entries) {
105
+ if (entry.strategy === "interop") continue;
106
+ const versions = versionsByPkg.get(entry.pkg) ?? [];
107
+ if (versions.length === 0) {
108
+ const at = entry.rangeSpan[1];
109
+ if (entry.peer && entry.strategy) {
110
+ const expected = yield* detectPeerDrift(entry).pipe(Effect.catchAll(() => Effect.succeed(null)));
111
+ if (expected !== null) {
112
+ edits.push({
113
+ span: entry.peer.span,
114
+ text: JSON.stringify(expected)
115
+ });
116
+ changedSpans.add(entry.rangeSpan[0]);
117
+ continue;
118
+ }
119
+ } else if (!entry.peer && entry.strategy) {
120
+ const peerRange = yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catchAll(() => Effect.succeed(null)));
121
+ if (peerRange !== null) {
122
+ edits.push({
123
+ span: [at, at],
124
+ text: `, peer: ${JSON.stringify(peerRange)}`
125
+ });
126
+ changedSpans.add(entry.rangeSpan[0]);
127
+ continue;
128
+ }
129
+ }
130
+ skipped.push(`${entry.catalog}.${entry.pkg}`);
131
+ continue;
132
+ }
133
+ const inRange = (yield* planEntry(entry, versions).pipe(Effect.catchAll(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
134
+ const at = entry.rangeSpan[1];
135
+ if (inRange) {
136
+ edits.push({
137
+ span: entry.rangeSpan,
138
+ text: JSON.stringify(inRange.range)
139
+ });
140
+ changedSpans.add(entry.rangeSpan[0]);
141
+ if (entry.peer && inRange.peerRange) edits.push({
142
+ span: entry.peer.span,
143
+ text: JSON.stringify(inRange.peerRange)
144
+ });
145
+ else if (!entry.peer && entry.strategy && inRange.peerRange) edits.push({
146
+ span: [at, at],
147
+ text: `, peer: ${JSON.stringify(inRange.peerRange)}`
148
+ });
149
+ } else if (!entry.peer && entry.strategy) {
150
+ const peerRange = yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catchAll(() => Effect.succeed(null)));
151
+ if (peerRange !== null) {
152
+ edits.push({
153
+ span: [at, at],
154
+ text: `, peer: ${JSON.stringify(peerRange)}`
155
+ });
156
+ changedSpans.add(entry.rangeSpan[0]);
157
+ }
158
+ } else if (entry.peer && entry.strategy) {
159
+ const expected = yield* detectPeerDrift(entry).pipe(Effect.catchAll(() => Effect.succeed(null)));
160
+ if (expected !== null) {
161
+ edits.push({
162
+ span: entry.peer.span,
163
+ text: JSON.stringify(expected)
164
+ });
165
+ changedSpans.add(entry.rangeSpan[0]);
166
+ }
167
+ }
168
+ }
169
+ const interopEntries = entries.filter((e) => e.strategy === "interop");
170
+ const conflicts = [];
171
+ const byCatalog = /* @__PURE__ */ new Map();
172
+ for (const e of interopEntries) {
173
+ const list = byCatalog.get(e.catalog) ?? [];
174
+ list.push(e);
175
+ byCatalog.set(e.catalog, list);
176
+ }
177
+ for (const [, group] of byCatalog) {
178
+ const members = [];
179
+ for (const e of group) {
180
+ const versions = versionsByPkg.get(e.pkg) ?? [];
181
+ const inRange = (yield* planEntry(e, versions).pipe(Effect.catchAll(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
182
+ const ceiling = inRange ? inRange.version : e.currentRange.replace(/^[\^~]/, "");
183
+ members.push({
184
+ pkg: e.pkg,
185
+ ceiling,
186
+ candidates: versions
187
+ });
188
+ }
189
+ const result = yield* runInterop(members, opts.resolver);
190
+ edits.push(...buildInteropEdits(group, result));
191
+ for (const e of group) if (interopEntryChanged(e, result)) changedSpans.add(e.rangeSpan[0]);
192
+ conflicts.push(...result.conflicts);
193
+ }
194
+ if (edits.length > 0) {
195
+ const next = applyEdits(source, edits);
196
+ yield* Effect.try({
197
+ try: () => writeFileSync(opts.file, next, "utf8"),
198
+ catch: () => new UpgradeError({ message: `Cannot write ${opts.file}` })
199
+ });
200
+ }
201
+ return {
202
+ updated: changedSpans.size,
203
+ skipped,
204
+ conflicts
205
+ };
206
+ });
207
+ }
208
+ /**
209
+ * Apply the interactive result when interop members are present: the
210
+ * non-interop decisions go through `buildEdits`, the interop members through
211
+ * their separately-computed span edits. Interop members are EXCLUDED from
212
+ * `buildEdits` so the two never emit a range edit over the same span (which
213
+ * `applyEdits` would reject as overlapping).
214
+ *
215
+ * @internal
216
+ */
217
+ function applyInteropAndDecisions(file, source, nonInteropDecisions, interopEdits) {
218
+ return Effect.gen(function* () {
219
+ const edits = [...buildEdits(nonInteropDecisions), ...interopEdits];
220
+ if (edits.length === 0) return;
221
+ const next = applyEdits(source, edits);
222
+ yield* Effect.try({
223
+ try: () => writeFileSync(file, next, "utf8"),
224
+ catch: () => new UpgradeError({ message: `Cannot write ${file}` })
225
+ });
226
+ });
227
+ }
228
+ /** Project walk items to the non-interactive default decisions (latest-in-range, plus peer-only keeps). @internal */
229
+ function projectDecisions(items, full) {
230
+ const out = [];
231
+ for (const i of items) {
232
+ const inRange = i.candidates.find((c) => c.kind === "in-range");
233
+ if (inRange) {
234
+ out.push({
235
+ item: i,
236
+ chosen: inRange
237
+ });
238
+ continue;
239
+ }
240
+ if (i.driftPeer !== null || i.materializePeer !== null) {
241
+ const keep = i.candidates.find((c) => c.kind === "keep");
242
+ if (keep) {
243
+ out.push({
244
+ item: i,
245
+ chosen: keep
246
+ });
247
+ continue;
248
+ }
249
+ }
250
+ if (full) {
251
+ const keep = i.candidates.find((c) => c.kind === "keep");
252
+ if (keep) out.push({
253
+ item: i,
254
+ chosen: keep
255
+ });
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ /** Build the colored preview summary string without writing. @internal */
261
+ function runUpgradePreview(opts) {
262
+ return Effect.gen(function* () {
263
+ const source = yield* Effect.try({
264
+ try: () => readFileSync(opts.file, "utf8"),
265
+ catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
266
+ });
267
+ const discovered = yield* Effect.try({
268
+ try: () => discoverCatalogEntries(source, opts.file),
269
+ catch: (e) => new UpgradeError({ message: String(e) })
270
+ });
271
+ const gate = yield* computeGate(source, opts.file, opts.resolver);
272
+ const versions = yield* resolveGatedVersions(discovered.entries, opts.resolver, gate, Date.now());
273
+ return renderSummary(projectDecisions(yield* buildWalkItems(discovered.entries, versions).pipe(Effect.catchAll((e) => Effect.fail(new UpgradeError({ message: e.message })))), opts.full), void 0, { color: opts.color ?? false });
274
+ });
275
+ }
276
+ /**
277
+ * Resolve the target file: the passed path, or autodetect in cwd.
278
+ *
279
+ * @internal
280
+ */
281
+ function resolveTargetFile(fileOpt) {
282
+ return Effect.gen(function* () {
283
+ const explicit = Option.getOrUndefined(fileOpt);
284
+ if (explicit !== void 0) return explicit;
285
+ const picked = pickConfigCandidate(yield* findConfigFiles(process.cwd()));
286
+ if (!picked.ok) return yield* Effect.fail(new UpgradeError({ message: picked.message }));
287
+ return picked.file;
288
+ });
289
+ }
290
+ const fileArg = Args.file({
291
+ name: "file",
292
+ exists: "yes"
293
+ }).pipe(Args.optional);
294
+ const yesFlag = Options.boolean("yes").pipe(Options.withAlias("y"), Options.withDefault(false));
295
+ const dryRunFlag = Options.boolean("dry-run").pipe(Options.withDefault(false));
296
+ const catalogOption = Options.text("catalog").pipe(Options.optional);
297
+ const previewFlag = Options.boolean("preview").pipe(Options.withDefault(false));
298
+ const fullFlag = Options.boolean("full").pipe(Options.withDefault(false));
299
+ /**
300
+ * The "upgrade" command. The default path runs the interactive walk;
301
+ * --yes applies latest-in-range non-interactively; --dry-run prints the
302
+ * summary without writing; --catalog restricts to a single catalog by name.
303
+ *
304
+ * @internal
305
+ */
306
+ const upgradeCommand = Command.make("upgrade", {
307
+ file: fileArg,
308
+ yes: yesFlag,
309
+ dryRun: dryRunFlag,
310
+ catalog: catalogOption,
311
+ preview: previewFlag,
312
+ full: fullFlag
313
+ }, ({ file: fileOpt, yes, dryRun, catalog, preview, full }) => Effect.gen(function* () {
314
+ const file = yield* resolveTargetFile(fileOpt);
315
+ const resolver = yield* RegistryResolver;
316
+ const caps = detectCapabilities();
317
+ if (preview) {
318
+ const text = yield* runUpgradePreview({
319
+ file,
320
+ resolver,
321
+ full,
322
+ color: caps.color
323
+ });
324
+ yield* Effect.sync(() => process.stdout.write(`${text}\n`));
325
+ return;
326
+ }
327
+ if (yes) {
328
+ const result = yield* runUpgrade({
329
+ file,
330
+ resolver,
331
+ ...caps.interactive ? { onProgress: writeResolveProgress } : {}
332
+ });
333
+ yield* Effect.sync(() => process.stdout.write(`Updated ${result.updated} package(s); skipped ${result.skipped.length}.\n`));
334
+ if (result.conflicts.length > 0) {
335
+ const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`).join("\n");
336
+ yield* Effect.sync(() => process.stdout.write(`Interop conflicts (left at your pick):\n${lines}\n`));
337
+ }
338
+ return;
339
+ }
340
+ const source = yield* Effect.try({
341
+ try: () => readFileSync(file, "utf8"),
342
+ catch: () => new UpgradeError({ message: `Cannot read ${file}` })
343
+ });
344
+ const discovered = yield* Effect.try({
345
+ try: () => discoverCatalogEntries(source, file),
346
+ catch: (e) => new UpgradeError({ message: String(e) })
347
+ });
348
+ const catalogName = Option.getOrUndefined(catalog);
349
+ const entries = filterEntriesByCatalog(discovered.entries, catalogName);
350
+ const versions = yield* resolveGatedVersions(entries, resolver, yield* computeGate(source, file, resolver), Date.now(), caps.interactive ? writeResolveProgress : void 0);
351
+ const items = yield* buildWalkItems(entries, versions).pipe(Effect.catchAll((e) => Effect.fail(new UpgradeError({ message: e.message }))));
352
+ if (dryRun) {
353
+ const decisions = projectDecisions(items, false);
354
+ yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, void 0, { color: caps.color })}\n`));
355
+ return;
356
+ }
357
+ if (!caps.interactive) {
358
+ const text = renderSummary(projectDecisions(items, full), void 0, { color: caps.color });
359
+ yield* Effect.sync(() => process.stdout.write(`${text}\n\n(non-interactive terminal — run with --yes to apply, or in a TTY to choose)\n`));
360
+ return;
361
+ }
362
+ const decisions = yield* runWalk(items);
363
+ const interopByCatalog = /* @__PURE__ */ new Map();
364
+ for (const e of entries) {
365
+ if (e.strategy !== "interop") continue;
366
+ const list = interopByCatalog.get(e.catalog) ?? [];
367
+ list.push(e);
368
+ interopByCatalog.set(e.catalog, list);
369
+ }
370
+ const nonInteropDecisions = decisions.filter((d) => d.item.entry.strategy !== "interop");
371
+ const interopEdits = [];
372
+ const adjustments = [];
373
+ const allConflicts = [];
374
+ let interopChanged = 0;
375
+ for (const [, group] of interopByCatalog) {
376
+ const pickOf = (pkg) => {
377
+ const d = decisions.find((dd) => dd.item.entry.pkg === pkg);
378
+ if (d) return d.chosen.version;
379
+ const ge = group.find((g) => g.pkg === pkg);
380
+ return ge ? ge.currentRange.replace(/^[\^~]/, "") : "";
381
+ };
382
+ let members = group.map((e) => ({
383
+ pkg: e.pkg,
384
+ ceiling: pickOf(e.pkg),
385
+ candidates: versions.get(e.pkg) ?? []
386
+ }));
387
+ const originalPick = new Map(members.map((m) => [m.pkg, m.ceiling]));
388
+ const peerCache = /* @__PURE__ */ new Map();
389
+ let result = yield* runInterop(members, resolver, peerCache);
390
+ for (let round = 0; round < members.length + 1; round++) {
391
+ const reentry = reentryCandidates(members, result);
392
+ if (reentry.length === 0) break;
393
+ const capEntries = group.filter((e) => reentry.some((rc) => rc.pkg === e.pkg));
394
+ const cappedVersions = /* @__PURE__ */ new Map();
395
+ for (const rc of reentry) {
396
+ const all = versions.get(rc.pkg) ?? [];
397
+ cappedVersions.set(rc.pkg, rc.cap === null ? all : yield* capVersions(all, rc.cap));
398
+ }
399
+ const reDecisions = yield* runWalk(yield* buildWalkItems(capEntries, cappedVersions).pipe(Effect.catchAll((err) => Effect.fail(new UpgradeError({ message: err.message })))));
400
+ const before = new Map(members.map((m) => [m.pkg, m.ceiling]));
401
+ members = members.map((m) => {
402
+ const rd = reDecisions.find((d) => d.item.entry.pkg === m.pkg);
403
+ return rd ? {
404
+ ...m,
405
+ ceiling: rd.chosen.version
406
+ } : m;
407
+ });
408
+ if (!members.some((m) => before.get(m.pkg) !== m.ceiling)) break;
409
+ result = yield* runInterop(members, resolver, peerCache);
410
+ }
411
+ interopEdits.push(...buildInteropEdits(group, result));
412
+ allConflicts.push(...result.conflicts);
413
+ for (const e of group) {
414
+ if (interopEntryChanged(e, result)) interopChanged++;
415
+ const version = result.resolved.get(e.pkg);
416
+ const original = originalPick.get(e.pkg);
417
+ if (version === void 0 || original === void 0 || version === original) continue;
418
+ adjustments.push({
419
+ catalog: e.catalog,
420
+ pkg: e.pkg,
421
+ from: `${e.operator}${original}`,
422
+ to: `${e.operator}${version}`,
423
+ peer: result.peers.get(e.pkg) ?? `^${version}`
424
+ });
425
+ }
426
+ }
427
+ yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, {
428
+ adjustments,
429
+ conflicts: allConflicts
430
+ }, { color: caps.color })}\n`));
431
+ yield* applyInteropAndDecisions(file, source, nonInteropDecisions, interopEdits);
432
+ const nonInteropChanged = nonInteropDecisions.filter((d) => d.chosen.kind !== "keep" || d.item.entry.peer !== void 0 && d.item.driftPeer !== null || d.item.entry.peer === void 0 && d.item.materializePeer !== null).length;
433
+ yield* Effect.sync(() => process.stdout.write(`Applied ${nonInteropChanged + interopChanged} change(s).\n`));
434
+ }).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeContext.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
435
+
436
+ //#endregion
437
+ export { UpgradeError, applyInteropAndDecisions, computeGate, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, upgradeCommand, writeResolveProgress };
@@ -0,0 +1,122 @@
1
+ //#region src/cli/diff/build.ts
2
+ function isObject(v) {
3
+ return v !== null && typeof v === "object" && !Array.isArray(v);
4
+ }
5
+ /** Worst kind among children: changed if any differs, added/removed if uniform, else unchanged. */
6
+ function rollup(children) {
7
+ if (children.length === 0) return "unchanged";
8
+ if (children.every((c) => c.kind === "added")) return "added";
9
+ if (children.every((c) => c.kind === "removed")) return "removed";
10
+ return children.some((c) => c.kind !== "unchanged") ? "changed" : "unchanged";
11
+ }
12
+ /** Build every node under a value that exists only on one side (added or removed). */
13
+ function uniform(key, path, value, kind, tag) {
14
+ const here = [...path, key];
15
+ const side = kind === "added" ? { after: value } : { before: value };
16
+ if (isObject(value)) {
17
+ const children = Object.keys(value).map((k) => uniform(k, here, value[k], kind));
18
+ return {
19
+ key,
20
+ path: here,
21
+ kind,
22
+ ...tag ? { tag } : {},
23
+ children
24
+ };
25
+ }
26
+ if (Array.isArray(value)) {
27
+ const children = value.map((el) => ({
28
+ ...uniform(String(el), here, el, kind),
29
+ arrayElement: true
30
+ }));
31
+ return {
32
+ key,
33
+ path: here,
34
+ kind,
35
+ ...tag ? { tag } : {},
36
+ children
37
+ };
38
+ }
39
+ return {
40
+ key,
41
+ path: here,
42
+ kind,
43
+ ...tag ? { tag } : {},
44
+ ...side
45
+ };
46
+ }
47
+ /** Diff two arrays as sets keyed by stringified element. */
48
+ function diffArray(key, path, before, after, tag) {
49
+ const here = [...path, key];
50
+ const b = new Set(before.map(String));
51
+ const a = new Set(after.map(String));
52
+ const children = [.../* @__PURE__ */ new Set([...b, ...a])].sort((x, y) => x.localeCompare(y)).map((el) => {
53
+ const inB = b.has(el);
54
+ const inA = a.has(el);
55
+ const kind = inB && inA ? "unchanged" : inA ? "added" : "removed";
56
+ const side = inA ? { after: el } : { before: el };
57
+ return {
58
+ key: el,
59
+ path: [...here, el],
60
+ kind,
61
+ arrayElement: true,
62
+ ...side
63
+ };
64
+ });
65
+ return {
66
+ key,
67
+ path: here,
68
+ kind: rollup(children),
69
+ ...tag ? { tag } : {},
70
+ children
71
+ };
72
+ }
73
+ function diffValue(key, path, before, after, tag) {
74
+ const here = [...path, key];
75
+ if (isObject(before) && isObject(after)) {
76
+ const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
77
+ if (!(k in before)) return uniform(k, here, after[k], "added");
78
+ if (!(k in after)) return uniform(k, here, before[k], "removed");
79
+ return diffValue(k, here, before[k], after[k]);
80
+ });
81
+ return {
82
+ key,
83
+ path: here,
84
+ kind: rollup(children),
85
+ ...tag ? { tag } : {},
86
+ children
87
+ };
88
+ }
89
+ if (Array.isArray(before) && Array.isArray(after)) return diffArray(key, path, before, after, tag);
90
+ return {
91
+ key,
92
+ path: here,
93
+ kind: JSON.stringify(before) === JSON.stringify(after) ? "unchanged" : "changed",
94
+ ...tag ? { tag } : {},
95
+ before,
96
+ after
97
+ };
98
+ }
99
+ /**
100
+ * Compare two canonicalized workspace objects into a diff tree. Top-level keys
101
+ * carry a `tag`: `local` when the key was sourced from `config.local`,
102
+ * `unmanaged` when the key is not in the plugin-managed set.
103
+ *
104
+ * @internal
105
+ */
106
+ function buildDiff(before, after, meta) {
107
+ const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
108
+ const tag = meta.localKeys.has(k) ? "local" : meta.managedKeys.has(k) ? void 0 : "unmanaged";
109
+ if (!(k in before)) return uniform(k, [], after[k], "added", tag);
110
+ if (!(k in after)) return uniform(k, [], before[k], "removed", tag);
111
+ return diffValue(k, [], before[k], after[k], tag);
112
+ });
113
+ return {
114
+ key: "",
115
+ path: [],
116
+ kind: rollup(children),
117
+ children
118
+ };
119
+ }
120
+
121
+ //#endregion
122
+ export { buildDiff };
@@ -0,0 +1,103 @@
1
+ //#region src/cli/diff/render.ts
2
+ const CONTEXT = 2;
3
+ const GUTTER = {
4
+ added: "+",
5
+ removed: "-",
6
+ changed: "~",
7
+ unchanged: " "
8
+ };
9
+ const STYLE = {
10
+ added: "added",
11
+ removed: "removed",
12
+ changed: "changed",
13
+ unchanged: "unchanged"
14
+ };
15
+ function scalarText(v) {
16
+ return typeof v === "string" ? v : JSON.stringify(v);
17
+ }
18
+ function flatten(node, depth) {
19
+ const indent = depth;
20
+ const gutter = GUTTER[node.kind];
21
+ const style = STYLE[node.kind];
22
+ const tag = node.tag;
23
+ const changed = node.kind !== "unchanged";
24
+ const isArrayEl = node.arrayElement === true;
25
+ if (node.children) return [{
26
+ line: {
27
+ indent,
28
+ gutter,
29
+ segments: [{
30
+ text: `${node.key}:`,
31
+ style
32
+ }],
33
+ ...tag ? { tag } : {}
34
+ },
35
+ changed
36
+ }, ...node.children.flatMap((c) => flatten(c, depth + 1))];
37
+ let text;
38
+ if (node.kind === "changed") text = `${node.key}: ${scalarText(node.before)} → ${scalarText(node.after)}`;
39
+ else if (isArrayEl) text = `- ${node.key}`;
40
+ else text = `${node.key}: ${scalarText(node.after ?? node.before)}`;
41
+ return [{
42
+ line: {
43
+ indent,
44
+ gutter,
45
+ segments: [{
46
+ text,
47
+ style
48
+ }],
49
+ ...tag ? { tag } : {}
50
+ },
51
+ changed
52
+ }];
53
+ }
54
+ /**
55
+ * Render a diff tree to styled lines in canonical-YAML shape. Default collapses
56
+ * unchanged lines outside a 2-line window around changes into a single
57
+ * "… N unchanged" marker; `full` keeps every line.
58
+ *
59
+ * @internal
60
+ */
61
+ function renderExportDiff(root, opts) {
62
+ const flats = (root.children ?? []).flatMap((c) => flatten(c, 0));
63
+ if (opts.full) return flats.map((f) => f.line);
64
+ const keep = new Array(flats.length).fill(false);
65
+ flats.forEach((f, i) => {
66
+ if (!f.changed) return;
67
+ for (let j = Math.max(0, i - CONTEXT); j <= Math.min(flats.length - 1, i + CONTEXT); j++) keep[j] = true;
68
+ });
69
+ for (let i = 0; i < flats.length; i++) {
70
+ if (!keep[i]) continue;
71
+ let depth = flats[i].line.indent;
72
+ for (let j = i - 1; j >= 0 && depth > 0; j--) if (flats[j].line.indent < depth) {
73
+ keep[j] = true;
74
+ depth = flats[j].line.indent;
75
+ }
76
+ }
77
+ const out = [];
78
+ let dropped = 0;
79
+ const flushDropped = () => {
80
+ if (dropped > 0) {
81
+ out.push({
82
+ indent: 0,
83
+ gutter: " ",
84
+ segments: [{
85
+ text: `… ${dropped} unchanged`,
86
+ style: "unchanged"
87
+ }]
88
+ });
89
+ dropped = 0;
90
+ }
91
+ };
92
+ flats.forEach((f, i) => {
93
+ if (keep[i]) {
94
+ flushDropped();
95
+ out.push(f.line);
96
+ } else dropped++;
97
+ });
98
+ flushDropped();
99
+ return out;
100
+ }
101
+
102
+ //#endregion
103
+ export { renderExportDiff };