@savvy-web/mcp 1.4.0 → 1.5.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@savvy-web%2Fmcp?label=npm&color=cb3837)](https://www.npmjs.com/package/@savvy-web/mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- The `savvy-mcp` [Model Context Protocol](https://modelcontextprotocol.io/) server. It serves [Silk Suite](https://github.com/savvy-web/systems) tooling to coding agents as structured tools, so an agent can read workspace facts and run Silk checks instead of parsing console output or guessing. It is a tools-only server — six tools, no resources.
6
+ The `savvy-mcp` [Model Context Protocol](https://modelcontextprotocol.io/) server. It serves [Silk Suite](https://github.com/savvy-web/systems) tooling to coding agents as structured tools, so an agent can read workspace facts and run Silk checks instead of parsing console output or guessing. It is a tools-only server — eight tools, no resources.
7
7
 
8
8
  ## Install
9
9
 
@@ -46,7 +46,9 @@ npx @modelcontextprotocol/inspector savvy-mcp .
46
46
  - `changeset_inspect` — read-only changeset analysis for release work: `mode=branch` diffs the current branch against its base and attributes every changed file to its owning package, `mode=config` surfaces the resolved `.changeset/config.json` with its release surfaces, version files and ignore list, and `mode=classify` reports how the branch's pending changesets classify each package's bump. It never writes a changeset. Backed by the same `silk-effects` changeset services the `savvy` CLI uses.
47
47
  - `changeset_validate` — read-only validation of the files in a changeset directory against the section-aware rules, returning typed diagnostics (file, rule, line, column, message) plus an ok flag and error count. Use it instead of shelling out to `savvy changeset lint`.
48
48
  - `changeset_preview` — read-only preview of the next release: it runs the genuine changesets engine over the pending changesets and returns each package's version bump (old to new) plus the rendered CHANGELOG block, exactly as it would ship. It never mutates the repo. Backed by the same `silk-effects` release planner the `savvy changeset version` command applies.
49
- - `biome_check` — run Biome over a path and get structured diagnostics back: `mode=check` (lint, format and organize-imports) or `mode=lint`. Unlike the other tools it can mutate pass `write` for safe fixes or `unsafe` for unsafe ones (both git-reversible) so it returns the same diagnostics the Biome LSP surfaces for files you have edited.
49
+ - `changeset_deps_detect` — read-only preview of the cumulative dependency diff (merge-base to working tree): one entry per affected workspace package with its resolved dependency-table rows, `catalog:`/`workspace:` specifiers resolved to concrete versions. It never writes a changeset. Backed by `silk-effects`' `Changesets.DepsRegen.plan`.
50
+ - `changeset_deps_regen` — regenerates pure-dependency changesets: deletes stale ones and writes fresh single-package, patch-bump changesets from the cumulative dependency diff. Mutating unless `dryRun` is set, in which case it reports what it would delete and write without touching the filesystem. Backed by `silk-effects`' `Changesets.DepsRegen`.
51
+ - `biome_check` — run Biome over a path and get structured diagnostics back: `mode=check` (lint, format and organize-imports) or `mode=lint`. Unlike most of the other tools it can mutate — pass `write` for safe fixes or `unsafe` for unsafe ones (both git-reversible) — so it returns the same diagnostics the Biome LSP surfaces for files you have edited.
50
52
 
51
53
  ## License
52
54
 
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { WorkspaceDiscoveryError, WorkspaceRoot } from "workspaces-effect";
4
4
  //#region src/context.d.ts
5
5
  /** The long-lived runtime and the project working directory. */
6
6
  interface McpContext {
7
- readonly runtime: ManagedRuntime.ManagedRuntime<SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner, WorkspaceDiscoveryError>;
7
+ readonly runtime: ManagedRuntime.ManagedRuntime<SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen, WorkspaceDiscoveryError>;
8
8
  readonly cwd: string;
9
9
  }
10
10
  //#endregion
@@ -12,16 +12,16 @@ interface McpContext {
12
12
  /**
13
13
  * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
14
14
  * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
15
- * `Changesets.ConfigInspector`, and `Changesets.ReleasePlanner`; requires
16
- * `CommandExecutor` + `FileSystem` + `Path` from the host's platform layer
17
- * (`NodeContext.layer` in bin.ts).
15
+ * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`, and
16
+ * `Changesets.DepsRegen`; requires `CommandExecutor` + `FileSystem` + `Path`
17
+ * from the host's platform layer (`NodeContext.layer` in bin.ts).
18
18
  *
19
19
  * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
20
20
  * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
21
21
  * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
22
22
  * host platform layer.
23
23
  */
24
- declare const SilkRuntimeLive: Layer.Layer<Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | import("@savvy-web/silk-effects").SilkWorkspaceAnalyzer | Turbo.TurboInspector | import("workspaces-effect").WorkspaceRoot, import("workspaces-effect").WorkspaceDiscoveryError, import("@effect/platform/CommandExecutor").CommandExecutor | import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path>;
24
+ declare const SilkRuntimeLive: Layer.Layer<Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.DepsRegen | Changesets.ReleasePlanner | import("@savvy-web/silk-effects").SilkWorkspaceAnalyzer | Turbo.TurboInspector | import("workspaces-effect").WorkspaceRoot, import("workspaces-effect").WorkspaceDiscoveryError, import("@effect/platform/CommandExecutor").CommandExecutor | import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path>;
25
25
  //#endregion
26
26
  //#region src/server.d.ts
27
27
  /** Build the server and connect it over stdio. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "description": "The savvy MCP server — Silk Suite tooling and library knowledge for coding agents",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/mcp",
@@ -36,7 +36,7 @@
36
36
  "@effect/rpc": "^0.75.1",
37
37
  "@effect/sql": "^0.51.1",
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@savvy-web/silk-effects": "1.5.2",
39
+ "@savvy-web/silk-effects": "1.6.0",
40
40
  "effect": "^3.21.4",
41
41
  "workspaces-effect": "^1.2.0",
42
42
  "zod": "^4.4.3"
package/runtime.js CHANGED
@@ -24,18 +24,36 @@ import { WorkspaceRootLive, WorkspacesLive } from "workspaces-effect";
24
24
  const DepsLive = Layer.mergeAll(WorkspacesLive, ChangesetConfigReaderLive, TagStrategyLive, VersioningStrategyLive.pipe(Layer.provide(ChangesetConfigReaderLive)));
25
25
  const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ReleasePlannerLive), Layer.provideMerge(Changesets.ConfigInspectorLive));
26
26
  /**
27
+ * `Changesets.DepsRegen` (the `changeset_deps_detect` / `changeset_deps_regen`
28
+ * orchestration service), fully composed except for the services supplied by
29
+ * {@link DepsLive} / the host platform layer.
30
+ *
31
+ * `DepsRegenLive` requires `WorkspaceSnapshotReader | ConfigInspector |
32
+ * WorkspaceDiscovery | CatalogResolver | PublishabilityDetector`. Here
33
+ * `ConfigInspector` is provided via the shared {@link InspectorAndAnalyzerLive}
34
+ * reference (so Effect memoizes the single `ConfigInspector` instance already
35
+ * merged into the runtime), and `WorkspaceSnapshotReader` via the
36
+ * dependency-free {@link Changesets.WorkspaceSnapshotReaderLive}. The remaining
37
+ * three — `WorkspaceDiscovery`, `CatalogResolver`, `PublishabilityDetector` —
38
+ * are left open and satisfied by `WorkspacesLive` inside {@link DepsLive}
39
+ * (unlike the CLI, whose minimal workspace trio has to compose
40
+ * `CatalogResolverLive`/`PublishabilityDetectorLive` by hand). `FileSystem` /
41
+ * `Path` / `CommandExecutor` flow up to the host `NodeContext.layer`.
42
+ */
43
+ const DepsRegenGroupLive = Changesets.DepsRegenLive.pipe(Layer.provide(InspectorAndAnalyzerLive), Layer.provide(Changesets.WorkspaceSnapshotReaderLive));
44
+ /**
27
45
  * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
28
46
  * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
29
- * `Changesets.ConfigInspector`, and `Changesets.ReleasePlanner`; requires
30
- * `CommandExecutor` + `FileSystem` + `Path` from the host's platform layer
31
- * (`NodeContext.layer` in bin.ts).
47
+ * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`, and
48
+ * `Changesets.DepsRegen`; requires `CommandExecutor` + `FileSystem` + `Path`
49
+ * from the host's platform layer (`NodeContext.layer` in bin.ts).
32
50
  *
33
51
  * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
34
52
  * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
35
53
  * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
36
54
  * host platform layer.
37
55
  */
38
- const SilkRuntimeLive = Layer.mergeAll(SilkWorkspaceAnalyzerLive, WorkspaceRootLive, Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), InspectorAndAnalyzerLive).pipe(Layer.provide(DepsLive));
56
+ const SilkRuntimeLive = Layer.mergeAll(SilkWorkspaceAnalyzerLive, WorkspaceRootLive, Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), InspectorAndAnalyzerLive, DepsRegenGroupLive).pipe(Layer.provide(DepsLive));
39
57
 
40
58
  //#endregion
41
59
  export { SilkRuntimeLive };
package/server.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { effectToZodSchema } from "./schema/effect-to-zod.js";
2
2
  import { BiomeCheckAsMarkdown, BiomeCheckResult, runBiomeCheck } from "./tools/biome-check.js";
3
+ import { ChangesetDepsDetectAsMarkdown, ChangesetDepsDetectResult, changesetDepsDetect } from "./tools/changeset-deps-detect.js";
4
+ import { ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenResult, changesetDepsRegen } from "./tools/changeset-deps-regen.js";
3
5
  import { ChangesetInspectAsMarkdown, ChangesetInspectResult, changesetInspect } from "./tools/changeset-inspect.js";
4
6
  import { ChangesetPreviewAsMarkdown, ChangesetPreviewResult, changesetPreview } from "./tools/changeset-preview.js";
5
7
  import { ChangesetValidateAsMarkdown, ChangesetValidateResult, changesetValidate } from "./tools/changeset-validate.js";
@@ -89,6 +91,19 @@ function buildServer(ctx) {
89
91
  const data = await ctx.runtime.runPromise(changesetValidate(args, ctx.cwd));
90
92
  return structuredResult(Schema.decodeSync(ChangesetValidateAsMarkdown)(data), data);
91
93
  });
94
+ server.registerTool("changeset_deps_detect", {
95
+ description: "Read-only preview of the cumulative dependency diff (merge-base -> working tree) per workspace package. Returns each affected package's resolved dependency-table rows (catalog:/workspace: specifiers resolved to concrete versions; devDependencies retained) as the exact rows a pure-dependency changeset would carry. Does NOT write or delete any file. Prefer this over shelling out to savvy changeset deps detect.",
96
+ inputSchema: {
97
+ base: z.optional(z.string()).describe("Override the base branch used to compute the merge-base."),
98
+ package: z.optional(z.string()).describe("Restrict output to a single workspace package."),
99
+ cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
100
+ },
101
+ outputSchema: effectToZodSchema(ChangesetDepsDetectResult),
102
+ annotations: { readOnlyHint: true }
103
+ }, async (args) => {
104
+ const data = await ctx.runtime.runPromise(changesetDepsDetect(args, ctx.cwd));
105
+ return structuredResult(Schema.decodeSync(ChangesetDepsDetectAsMarkdown)(data), data);
106
+ });
92
107
  server.registerTool("changeset_preview", {
93
108
  description: "Read-only preview of the next release. Runs the genuine changesets engine over the pending changesets and returns each package's version bump (old -> new) plus the rendered CHANGELOG block (dependency tables included), exactly as it would ship. Does not modify the repo. Prefer this over hand-merging changeset files.",
94
109
  inputSchema: { cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.") },
@@ -98,6 +113,23 @@ function buildServer(ctx) {
98
113
  const data = await ctx.runtime.runPromise(changesetPreview(args, ctx.cwd));
99
114
  return structuredResult(Schema.decodeSync(ChangesetPreviewAsMarkdown)(data), data);
100
115
  });
116
+ server.registerTool("changeset_deps_regen", {
117
+ description: "Regenerate pure-dependency changesets: delete stale single-package Dependencies-only changesets and write fresh single-package, patch-bump changesets from the cumulative dependency diff (catalog:/workspace: resolved; devDependencies dropped). Mixed changesets (Dependencies plus other content) are left untouched. Set dryRun=true to preview the plan without touching the filesystem. NOTE: without dryRun this tool MUTATES .changeset/*.md (git-reversible). Prefer this over shelling out to savvy changeset deps regen.",
118
+ inputSchema: {
119
+ base: z.optional(z.string()).describe("Override the base branch used to compute the merge-base."),
120
+ package: z.optional(z.string()).describe("Restrict regeneration to a single workspace package."),
121
+ dryRun: z.optional(z.boolean()).describe("Compute the plan without writing or deleting any file."),
122
+ cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
123
+ },
124
+ outputSchema: effectToZodSchema(ChangesetDepsRegenResult),
125
+ annotations: {
126
+ destructiveHint: true,
127
+ idempotentHint: false
128
+ }
129
+ }, async (args) => {
130
+ const data = await ctx.runtime.runPromise(changesetDepsRegen(args, ctx.cwd));
131
+ return structuredResult(Schema.decodeSync(ChangesetDepsRegenAsMarkdown)(data), data);
132
+ });
101
133
  server.registerTool("biome_check", {
102
134
  description: "Run Biome over a path and get structured diagnostics back. mode=check (default; lint + format + organize-imports) or mode=lint. Set write=true to apply safe fixes (--write), unsafe=true for unsafe fixes (--write --unsafe). Prefer this over shelling out to biome; the LSP already covers files you've edited. Returns markdown in content[] and a typed object in structuredContent. NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
103
135
  inputSchema: {
@@ -0,0 +1,88 @@
1
+ import { Changesets } from "@savvy-web/silk-effects";
2
+ import { Effect, ParseResult, Schema } from "effect";
3
+ import { WorkspaceRoot } from "workspaces-effect";
4
+
5
+ //#region src/tools/changeset-deps-detect.ts
6
+ /**
7
+ * The `changeset_deps_detect` MCP tool: a read-only preview of the cumulative
8
+ * dependency diff (merge-base → working tree) over silk-effects'
9
+ * `Changesets.DepsRegen.plan`. Returns one entry per affected workspace package
10
+ * — its resolved dependency-table rows (devDependencies retained) — plus a
11
+ * one-way markdown transform. Read-only: no changeset file is written or
12
+ * deleted.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /** One affected workspace package's resolved dependency diff. */
17
+ const ChangesetDepsDetectPackage = Schema.Struct({
18
+ package: Schema.String,
19
+ relativePath: Schema.String,
20
+ rows: Schema.Array(Changesets.DependencyTableRowSchema)
21
+ }).annotations({ identifier: "ChangesetDepsDetectPackage" });
22
+ /** The `changeset_deps_detect` tool result. */
23
+ const ChangesetDepsDetectResult = Schema.Struct({
24
+ root: Schema.String,
25
+ packages: Schema.Array(ChangesetDepsDetectPackage)
26
+ }).annotations({
27
+ identifier: "ChangesetDepsDetectResult",
28
+ title: "changeset_deps_detect result",
29
+ description: "Read-only per-package dependency diff (devDependencies retained). No files are written."
30
+ });
31
+ /**
32
+ * Render a repo-derived value as an inert markdown code span so a crafted path,
33
+ * package, or dependency name cannot inject markdown structure into the
34
+ * transcript an agent reads. Control characters (which would break table rows
35
+ * and headings) are flattened to spaces, table-cell pipes are escaped, and the
36
+ * span is fenced with a backtick run longer than any in the value — CommonMark
37
+ * forbids backslash-escaping a backtick inside a code span.
38
+ */
39
+ const mdInline = (value) => {
40
+ const safe = value.replace(/\p{Cc}/gu, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
41
+ const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
42
+ const fence = "`".repeat(longest + 1);
43
+ const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
44
+ return `${fence}${pad}${safe}${pad}${fence}`;
45
+ };
46
+ /** Render the structured result as a markdown transcript. */
47
+ const renderMarkdown = (data) => {
48
+ if (data.packages.length === 0) return `# changeset deps detect — ${mdInline(data.root)}\n\nNo dependency changes detected.`;
49
+ const lines = [`# changeset deps detect — ${mdInline(data.root)}`, ``];
50
+ for (const pkg of data.packages) {
51
+ lines.push(`## ${mdInline(pkg.package)} — ${mdInline(pkg.relativePath)}`, ``);
52
+ lines.push(`| Dependency | Type | Action | From | To |`, `| --- | --- | --- | --- | --- |`);
53
+ for (const r of pkg.rows) lines.push(`| ${mdInline(r.dependency)} | ${r.type} | ${r.action} | ${mdInline(r.from)} | ${mdInline(r.to)} |`);
54
+ lines.push(``);
55
+ }
56
+ return lines.join("\n").trimEnd();
57
+ };
58
+ /** One-way transform: result to markdown. Encoding back is forbidden. */
59
+ const ChangesetDepsDetectAsMarkdown = Schema.transformOrFail(ChangesetDepsDetectResult, Schema.String, {
60
+ strict: true,
61
+ decode: (data) => ParseResult.succeed(renderMarkdown(data)),
62
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetDepsDetectAsMarkdown is one-way: markdown cannot be parsed back."))
63
+ });
64
+ /**
65
+ * Effect handler: resolve the workspace root, then compute the cumulative
66
+ * dependency diff via {@link Changesets.DepsRegen.plan} with `includeDevDeps`
67
+ * so devDependency rows are retained. Maps `plan.toWrite` into the structured
68
+ * result. No filesystem mutation happens (no `execute`).
69
+ */
70
+ const changesetDepsDetect = (args, fallbackCwd) => Effect.gen(function* () {
71
+ const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
72
+ return {
73
+ root,
74
+ packages: (yield* (yield* Changesets.DepsRegen).plan({
75
+ cwd: root,
76
+ includeDevDeps: true,
77
+ ...args.base ? { base: args.base } : {},
78
+ ...args.package ? { package: args.package } : {}
79
+ })).toWrite.map((entry) => ({
80
+ package: entry.diff.package,
81
+ relativePath: entry.diff.relativePath,
82
+ rows: entry.diff.rows
83
+ }))
84
+ };
85
+ });
86
+
87
+ //#endregion
88
+ export { ChangesetDepsDetectAsMarkdown, ChangesetDepsDetectPackage, ChangesetDepsDetectResult, changesetDepsDetect };
@@ -0,0 +1,100 @@
1
+ import { Changesets } from "@savvy-web/silk-effects";
2
+ import { Effect, ParseResult, Schema } from "effect";
3
+ import { WorkspaceRoot } from "workspaces-effect";
4
+
5
+ //#region src/tools/changeset-deps-regen.ts
6
+ /**
7
+ * The `changeset_deps_regen` MCP tool: delete stale pure-dependency changesets
8
+ * and write fresh single-package, patch-bump changesets from the cumulative
9
+ * dependency diff, over silk-effects' `Changesets.DepsRegen`. Mutating (writes
10
+ * and deletes `.changeset/*.md`) unless `dryRun` is set. The second mutating
11
+ * tool after `biome_check`; no `readOnlyHint`.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ /** The `changeset_deps_regen` tool result. */
16
+ const ChangesetDepsRegenResult = Schema.Struct({
17
+ root: Schema.String,
18
+ deleted: Schema.Array(Schema.String),
19
+ written: Schema.Array(Schema.String),
20
+ skippedMixed: Schema.Array(Schema.String),
21
+ dryRun: Schema.Boolean
22
+ }).annotations({
23
+ identifier: "ChangesetDepsRegenResult",
24
+ title: "changeset_deps_regen result",
25
+ description: "Regenerated pure-dependency changesets. Mutates .changeset/*.md unless dryRun is set."
26
+ });
27
+ /**
28
+ * Render a repo-derived value (path) as an inert markdown code span. Control
29
+ * characters are flattened to spaces and the span is fenced with a backtick run
30
+ * longer than any in the value — CommonMark forbids backslash-escaping a
31
+ * backtick inside a code span.
32
+ */
33
+ const mdInline = (value) => {
34
+ const safe = value.replace(/\p{Cc}/gu, " ");
35
+ const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
36
+ const fence = "`".repeat(longest + 1);
37
+ const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
38
+ return `${fence}${pad}${safe}${pad}${fence}`;
39
+ };
40
+ /** Render the structured result as a markdown transcript. */
41
+ const renderMarkdown = (data) => {
42
+ const heading = `# changeset deps regen — ${mdInline(data.root)}${data.dryRun ? " (dry run)" : ""}`;
43
+ if (data.deleted.length === 0 && data.written.length === 0 && data.skippedMixed.length === 0) return `${heading}\n\nNo dependency changes to regenerate.`;
44
+ const lines = [heading, ``];
45
+ const verb = data.dryRun ? "Would delete" : "Deleted";
46
+ if (data.deleted.length > 0) {
47
+ lines.push(`${verb} ${data.deleted.length} pure dependency changeset(s):`);
48
+ for (const file of data.deleted) lines.push(`- ${mdInline(file)}`);
49
+ lines.push(``);
50
+ }
51
+ if (data.written.length > 0) {
52
+ lines.push(`${data.dryRun ? "Would write" : "Wrote"} ${data.written.length} fresh dependency changeset(s):`);
53
+ for (const file of data.written) lines.push(`- ${mdInline(file)}`);
54
+ lines.push(``);
55
+ }
56
+ if (data.skippedMixed.length > 0) {
57
+ lines.push(`Skipped ${data.skippedMixed.length} mixed changeset(s):`);
58
+ for (const file of data.skippedMixed) lines.push(`- ${mdInline(file)}`);
59
+ }
60
+ return lines.join("\n").trimEnd();
61
+ };
62
+ /** One-way transform: result to markdown. Encoding back is forbidden. */
63
+ const ChangesetDepsRegenAsMarkdown = Schema.transformOrFail(ChangesetDepsRegenResult, Schema.String, {
64
+ strict: true,
65
+ decode: (data) => ParseResult.succeed(renderMarkdown(data)),
66
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetDepsRegenAsMarkdown is one-way: markdown cannot be parsed back."))
67
+ });
68
+ /**
69
+ * Effect handler: resolve the workspace root, compute a {@link Changesets.RegenPlan}
70
+ * via {@link Changesets.DepsRegen.plan}, then — unless `dryRun` — apply it via
71
+ * `execute`. On a dry run the reported `deleted`/`written` reflect the plan's
72
+ * intended files without touching the filesystem.
73
+ */
74
+ const changesetDepsRegen = (args, fallbackCwd) => Effect.gen(function* () {
75
+ const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
76
+ const service = yield* Changesets.DepsRegen;
77
+ const plan = yield* service.plan({
78
+ cwd: root,
79
+ ...args.base ? { base: args.base } : {},
80
+ ...args.package ? { package: args.package } : {}
81
+ });
82
+ if (args.dryRun === true) return {
83
+ root,
84
+ deleted: plan.toDelete.map((entry) => entry.file),
85
+ written: plan.toWrite.map((entry) => entry.file),
86
+ skippedMixed: [...plan.skippedMixed],
87
+ dryRun: true
88
+ };
89
+ const result = yield* service.execute(plan);
90
+ return {
91
+ root,
92
+ deleted: [...result.deleted],
93
+ written: [...result.written],
94
+ skippedMixed: [...result.skippedMixed],
95
+ dryRun: false
96
+ };
97
+ });
98
+
99
+ //#endregion
100
+ export { ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenResult, changesetDepsRegen };