@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 +4 -2
- package/index.d.ts +5 -5
- package/package.json +2 -2
- package/runtime.js +22 -4
- package/server.js +32 -0
- package/tools/changeset-deps-detect.js +88 -0
- package/tools/changeset-deps-regen.js +100 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@savvy-web/mcp)
|
|
4
4
|
[](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 —
|
|
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
|
-
- `
|
|
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`,
|
|
16
|
-
* `CommandExecutor` + `FileSystem` + `Path`
|
|
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.
|
|
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.
|
|
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`,
|
|
30
|
-
* `CommandExecutor` + `FileSystem` + `Path`
|
|
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 };
|