@savvy-web/mcp 2.1.0 → 2.2.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 +2 -2
- package/index.d.ts +15 -3
- package/package.json +4 -3
- package/runtime.js +9 -4
- package/server.js +22 -6
- package/tools/repos-inspect.js +101 -5
- package/tools/repos-manage.js +99 -5
package/README.md
CHANGED
|
@@ -49,8 +49,8 @@ npx @modelcontextprotocol/inspector savvy-mcp .
|
|
|
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
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
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.
|
|
52
|
-
- `repos_inspect` — read-only inspection of vendored repositories: `mode=status` reports per-repo presence, the gitlink commit, working-tree dirtiness, and stale note ids; `mode=config` surfaces the validated `.repos/config.json` manifest and its entries. Returns markdown-escaped output since vendored-repo content is untrusted input. Backed by the same `silk-effects` `Repos` services the `savvy` CLI uses.
|
|
53
|
-
- `repos_manage` — manages vendored repositories (mutating counterpart to repos_inspect): `action=sync` initializes any missing submodules (`git submodule update --init --depth 1`), applies sparse-checkout from the manifest, and clears stale git locks; `action=pin` fetches and checks out the new ref, staging the updated gitlink and manifest; `action=add` adds a new repo entry; `action=note` appends a short note to a repo. Pin and
|
|
52
|
+
- `repos_inspect` — read-only inspection of vendored repositories: `mode=status` reports per-repo presence, the gitlink commit, working-tree dirtiness, and stale note ids; `mode=config` surfaces the validated `.repos/config.json` manifest and its entries; `mode=drift` reconciles the manifest, `.gitmodules`, the worktree, and `git submodule status`, reporting any mismatch as a typed drift kind; `mode=gitmodules` lists the parsed `.gitmodules` entries. Returns markdown-escaped output since vendored-repo content is untrusted input. Backed by the same `silk-effects` `Repos` services the `savvy` CLI uses.
|
|
53
|
+
- `repos_manage` — manages vendored repositories (mutating counterpart to repos_inspect): `action=sync` initializes any missing submodules (`git submodule update --init --depth 1`), applies sparse-checkout from the manifest, and clears stale git locks; `action=pin` fetches and checks out the new ref, staging the updated gitlink and manifest; `action=add` adds a new repo entry; `action=note` appends a short note to a repo; `action=remove` unvendors an entry; `action=rename` renames one in place; `action=restore` hard-resets one or more dirty checkouts back to their pinned commit. Pin, add, remove and rename stage git changes for the caller to commit; restore only resets the submodule's own working tree to its already-staged or already-committed gitlink commit, so there is nothing new to stage. `sync`, `pin`, `add`, `remove`, `rename` and `restore` unlock the vendored tree for the duration of their git work and leave it read-only again afterwards (files `0444`, directories `0555` — an executable file locks at `0555` instead, unlocking back to `0755` rather than losing its executable bit), so an agent that hits `EACCES` writing into `.repos/` should route the change through this tool rather than `chmod`. Backed by the same `silk-effects` `Repos` services the `savvy` CLI uses.
|
|
54
54
|
|
|
55
55
|
## License
|
|
56
56
|
|
package/index.d.ts
CHANGED
|
@@ -3,9 +3,21 @@ import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk
|
|
|
3
3
|
import { FileSystem, Layer, ManagedRuntime, Path } from "effect";
|
|
4
4
|
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
5
5
|
import "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import "zod";
|
|
6
7
|
//#region src/context.d.ts
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Every service the MCP runtime provides to the tool handlers.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* `FileSystem.FileSystem | Path.Path` are here so `repos_inspect`'s
|
|
13
|
+
* `gitmodules` mode can read `.gitmodules` through the ambient service
|
|
14
|
+
* (mirroring `Repos.ReposDrift.check`) rather than a bare `node:fs` import —
|
|
15
|
+
* `makeSilkRuntimeLayer` passes both through onto its own output via an
|
|
16
|
+
* `Effect.context` identity layer, so they survive `bin.ts`'s
|
|
17
|
+
* `Layer.provide(NodeServices.layer)` instead of being fully discharged by
|
|
18
|
+
* it.
|
|
19
|
+
*/
|
|
20
|
+
type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore | Repos.ReposDrift | FileSystem.FileSystem | Path.Path;
|
|
9
21
|
/** The long-lived runtime and the project working directory. */
|
|
10
22
|
interface McpContext {
|
|
11
23
|
readonly runtime: ManagedRuntime.ManagedRuntime<McpServices, never>;
|
|
@@ -18,7 +30,7 @@ interface McpContext {
|
|
|
18
30
|
* `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
|
|
19
31
|
* `Changesets.BranchAnalyzer`, `Changesets.ConfigInspector`,
|
|
20
32
|
* `Changesets.ReleasePlanner`, `Changesets.DepsRegen`, `Repos.ReposManager`,
|
|
21
|
-
* and `Repos.
|
|
33
|
+
* `Repos.ReposConfigStore`, and `Repos.ReposDrift`; requires `ChildProcessSpawner` + `FileSystem`
|
|
22
34
|
* + `Path` from the host's platform layer (`NodeServices.layer` in bin.ts).
|
|
23
35
|
*
|
|
24
36
|
* @remarks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@effect/platform-node": "4.0.0-beta.101",
|
|
35
35
|
"@effected/commands": "^0.3.1",
|
|
36
|
-
"@effected/
|
|
36
|
+
"@effected/git": "^0.6.0",
|
|
37
|
+
"@effected/workspaces": "^0.10.2",
|
|
37
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
|
-
"@savvy-web/silk-effects": "5.
|
|
39
|
+
"@savvy-web/silk-effects": "5.5.0",
|
|
39
40
|
"effect": "4.0.0-beta.101",
|
|
40
41
|
"zod": "^4.4.3"
|
|
41
42
|
}
|
package/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ToolDiscovery } from "@effected/commands";
|
|
2
2
|
import { Workspaces } from "@effected/workspaces";
|
|
3
3
|
import { ChangesetConfig, ChangesetConfigReader, Changesets, Repos, SilkPublishability, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
|
|
4
|
-
import { Layer } from "effect";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
5
|
|
|
6
6
|
//#region src/runtime.ts
|
|
7
7
|
/**
|
|
@@ -21,7 +21,7 @@ import { Layer } from "effect";
|
|
|
21
21
|
* `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
|
|
22
22
|
* `Changesets.BranchAnalyzer`, `Changesets.ConfigInspector`,
|
|
23
23
|
* `Changesets.ReleasePlanner`, `Changesets.DepsRegen`, `Repos.ReposManager`,
|
|
24
|
-
* and `Repos.
|
|
24
|
+
* `Repos.ReposConfigStore`, and `Repos.ReposDrift`; requires `ChildProcessSpawner` + `FileSystem`
|
|
25
25
|
* + `Path` from the host's platform layer (`NodeServices.layer` in bin.ts).
|
|
26
26
|
*
|
|
27
27
|
* @remarks
|
|
@@ -65,9 +65,14 @@ const makeSilkRuntimeLayer = (cwd) => {
|
|
|
65
65
|
* Same reference — one store instance. `ReposLockdown.layer` needs only the
|
|
66
66
|
* platform services, which flow up from the outer `NodeServices.layer`
|
|
67
67
|
* provision like `ReposManager`'s own `FileSystem`/`Path` requirement does.
|
|
68
|
+
* `ReposDrift.layer` is read-only (no `ReposLockdown` interaction) and
|
|
69
|
+
* requires only `ReposConfigStore | Git | FileSystem | Path`; `Git` comes
|
|
70
|
+
* from `kitGraph` below via the same one-reference discipline, and
|
|
71
|
+
* `repos_inspect`'s drift mode resolves it directly.
|
|
68
72
|
*/
|
|
69
|
-
const repos = Layer.mergeAll(Repos.ReposConfigStore.layer, Repos.ReposManager.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer)));
|
|
70
|
-
|
|
73
|
+
const repos = Layer.mergeAll(Repos.ReposConfigStore.layer, Repos.ReposManager.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer)), Repos.ReposDrift.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer)));
|
|
74
|
+
const platformPassthrough = Layer.effectContext(Effect.context());
|
|
75
|
+
return Layer.mergeAll(SilkWorkspaceAnalyzer.layer.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspector.layer.pipe(Layer.provide(toolDiscovery)), inspectorAndAnalyzer, depsRegen, repos, platformPassthrough).pipe(Layer.provideMerge(kitGraph));
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
//#endregion
|
package/server.js
CHANGED
|
@@ -22,6 +22,17 @@ import { z } from "zod";
|
|
|
22
22
|
*
|
|
23
23
|
* @packageDocumentation
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The `repos_inspect` wire-level `mode` enum. Exported so tests can assert
|
|
27
|
+
* the boundary rejects an unknown mode without duplicating the enum's
|
|
28
|
+
* member list.
|
|
29
|
+
*/
|
|
30
|
+
const ReposInspectModeSchema = z.enum([
|
|
31
|
+
"status",
|
|
32
|
+
"config",
|
|
33
|
+
"drift",
|
|
34
|
+
"gitmodules"
|
|
35
|
+
]).describe("status = drift report; config = the full agent brief; drift = four-authority submodule reconciliation; gitmodules = decoded .gitmodules sections.");
|
|
25
36
|
/** Wrap a markdown string + structured object in the dual-channel tool result. */
|
|
26
37
|
const structuredResult = (text, structured) => ({
|
|
27
38
|
content: [{
|
|
@@ -145,9 +156,9 @@ function buildServer(ctx) {
|
|
|
145
156
|
});
|
|
146
157
|
server.registerTool("repos_inspect", {
|
|
147
158
|
title: "Inspect vendored repos",
|
|
148
|
-
description: "Read-only: drift report or parsed .repos/config.json manifest with orientation and notes.",
|
|
159
|
+
description: "Read-only: drift report or parsed .repos/config.json manifest with orientation and notes. mode=status is the per-repo drift summary from ReposManager (present/dirty/commit); mode=config is the parsed manifest; mode=drift reconciles all four submodule authorities (manifest, .gitmodules, worktree, git submodule status) and reports every disagreement; mode=gitmodules decodes the raw .gitmodules file's submodule sections.",
|
|
149
160
|
inputSchema: {
|
|
150
|
-
mode:
|
|
161
|
+
mode: ReposInspectModeSchema,
|
|
151
162
|
cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
|
|
152
163
|
},
|
|
153
164
|
outputSchema: effectToZodSchema(ReposInspectResult),
|
|
@@ -159,15 +170,19 @@ function buildServer(ctx) {
|
|
|
159
170
|
});
|
|
160
171
|
server.registerTool("repos_manage", {
|
|
161
172
|
title: "Manage vendored repos",
|
|
162
|
-
description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo),
|
|
173
|
+
description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo), note (add/remove/promote an agent note), remove (unvendor a repo), rename (rename a vendored repo's manifest key and worktree), or restore (hard-reset a repo's worktree back to its pinned gitlink commit and re-apply sparse paths — DESTRUCTIVE to uncommitted worktree edits; never run implicitly). Pass action plus the fields that action needs: pin needs name+ref; add needs url+ref+purpose (name/sparse optional); note needs name+op, plus note (op=add), id (op=remove), or id+into (op=promote); remove needs name; rename needs name (the old name) + newName; restore takes an optional names list — omitted, it restores every dirty repo and reports the clean ones as skipped; given, it restores exactly those repos even if already clean. A decode failure names the missing field. The pin result's markdown surfaces commitMessage and staleNoteIds — review and commit after pinning. The remove result's markdown surfaces commitMessage and removedNotes — promote any durable notes elsewhere, then review and commit. The rename result's markdown surfaces commitMessage — review and commit after renaming. The restore result's markdown names exactly what was discarded.",
|
|
163
174
|
inputSchema: {
|
|
164
175
|
action: z.enum([
|
|
165
176
|
"sync",
|
|
166
177
|
"pin",
|
|
167
178
|
"add",
|
|
168
|
-
"note"
|
|
179
|
+
"note",
|
|
180
|
+
"remove",
|
|
181
|
+
"rename",
|
|
182
|
+
"restore"
|
|
169
183
|
]).describe("Which mutation to perform."),
|
|
170
|
-
name: z.optional(z.string()).describe("Repo name (pin, note; optional override for add)."),
|
|
184
|
+
name: z.optional(z.string()).describe("Repo name (pin, note, remove, rename; optional override for add)."),
|
|
185
|
+
newName: z.optional(z.string()).describe("New repo name (rename)."),
|
|
171
186
|
ref: z.optional(z.string()).describe("Git ref to pin/vendor to (pin, add)."),
|
|
172
187
|
url: z.optional(z.string()).describe("Repo URL to vendor (add)."),
|
|
173
188
|
purpose: z.optional(z.string()).describe("One-line purpose for the manifest (add)."),
|
|
@@ -180,6 +195,7 @@ function buildServer(ctx) {
|
|
|
180
195
|
note: z.optional(z.string()).describe("Note text (note, op=add)."),
|
|
181
196
|
id: z.optional(z.string()).describe("Note id (note, op=remove|promote)."),
|
|
182
197
|
into: z.optional(z.enum(["layout", "startHere"])).describe("Orientation target (note, op=promote)."),
|
|
198
|
+
names: z.optional(z.array(z.string())).describe("Repo names to restore (restore); omitted restores every dirty repo."),
|
|
183
199
|
cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
|
|
184
200
|
},
|
|
185
201
|
outputSchema: effectToZodSchema(ReposManageResult),
|
|
@@ -218,4 +234,4 @@ async function startMcpServer(ctx) {
|
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
//#endregion
|
|
221
|
-
export { buildServer, startMcpServer };
|
|
237
|
+
export { ReposInspectModeSchema, buildServer, startMcpServer };
|
package/tools/repos-inspect.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
import { mdInline } from "./md-inline.js";
|
|
2
2
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
3
3
|
import { Repos } from "@savvy-web/silk-effects";
|
|
4
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
4
|
+
import { Effect, FileSystem, Option, Path, Result, Schema, SchemaGetter } from "effect";
|
|
5
|
+
import { Gitmodules } from "@effected/git";
|
|
5
6
|
|
|
6
7
|
//#region src/tools/repos-inspect.ts
|
|
8
|
+
/**
|
|
9
|
+
* The `repos_inspect` MCP tool: a discriminated-union result keyed by `mode`
|
|
10
|
+
* (status | config), each variant embedding the corresponding resolved-output
|
|
11
|
+
* schema from silk-effects' Repos namespace, plus a one-way markdown
|
|
12
|
+
* transform. Read-only.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/** One `.gitmodules` submodule section, decoded into typed fields. */
|
|
17
|
+
const GitmodulesEntrySchema = Schema.Struct({
|
|
18
|
+
name: Schema.String,
|
|
19
|
+
path: Schema.String,
|
|
20
|
+
url: Schema.String,
|
|
21
|
+
branch: Schema.optionalKey(Schema.String),
|
|
22
|
+
shallow: Schema.optionalKey(Schema.Boolean),
|
|
23
|
+
update: Schema.optionalKey(Schema.String),
|
|
24
|
+
ignore: Schema.optionalKey(Schema.Literals([
|
|
25
|
+
"all",
|
|
26
|
+
"dirty",
|
|
27
|
+
"untracked",
|
|
28
|
+
"none"
|
|
29
|
+
])),
|
|
30
|
+
fetchRecurseSubmodules: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Literal("on-demand")]))
|
|
31
|
+
}).annotate({ identifier: "GitmodulesEntry" });
|
|
7
32
|
/** Status-report variant. */
|
|
8
33
|
const ReposStatusResult = Schema.Struct({
|
|
9
34
|
mode: Schema.Literal("status"),
|
|
@@ -14,11 +39,27 @@ const ReposConfigResult = Schema.Struct({
|
|
|
14
39
|
mode: Schema.Literal("config"),
|
|
15
40
|
result: Repos.ReposManifestFile
|
|
16
41
|
}).annotate({ identifier: "ReposConfigResult" });
|
|
42
|
+
/** Four-authority drift-reconciliation variant. */
|
|
43
|
+
const ReposDriftResult = Schema.Struct({
|
|
44
|
+
mode: Schema.Literal("drift"),
|
|
45
|
+
report: Repos.ReposDriftReport
|
|
46
|
+
}).annotate({ identifier: "ReposDriftResult" });
|
|
47
|
+
/** Raw `.gitmodules` variant: the decoded submodule sections, or a parse error. */
|
|
48
|
+
const ReposGitmodulesResult = Schema.Struct({
|
|
49
|
+
mode: Schema.Literal("gitmodules"),
|
|
50
|
+
entries: Schema.Array(GitmodulesEntrySchema),
|
|
51
|
+
parseError: Schema.optionalKey(Schema.String)
|
|
52
|
+
}).annotate({ identifier: "ReposGitmodulesResult" });
|
|
17
53
|
/** The `repos_inspect` tool result — a discriminated union keyed by `mode`. */
|
|
18
|
-
const ReposInspectResult = Schema.Union([
|
|
54
|
+
const ReposInspectResult = Schema.Union([
|
|
55
|
+
ReposStatusResult,
|
|
56
|
+
ReposConfigResult,
|
|
57
|
+
ReposDriftResult,
|
|
58
|
+
ReposGitmodulesResult
|
|
59
|
+
]).annotate({
|
|
19
60
|
identifier: "ReposInspectResult",
|
|
20
61
|
title: "repos_inspect result",
|
|
21
|
-
description: "Drift report (status)
|
|
62
|
+
description: "Drift report (status), the parsed manifest (config), the four-authority reconciliation report (drift), or the decoded .gitmodules sections (gitmodules)."
|
|
22
63
|
});
|
|
23
64
|
/** Render the structured result as a markdown transcript. */
|
|
24
65
|
const renderMarkdown = (data) => {
|
|
@@ -33,7 +74,7 @@ const renderMarkdown = (data) => {
|
|
|
33
74
|
`## Repos`
|
|
34
75
|
];
|
|
35
76
|
for (const entry of r.repos) {
|
|
36
|
-
lines.push(`### ${mdInline(entry.name)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`, `- present: ${entry.present}`, `-
|
|
77
|
+
lines.push(`### ${mdInline(entry.name)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`, `- present: ${entry.present}`, `- stagedCommit: ${entry.stagedCommit ? mdInline(entry.stagedCommit) : "(none)"}`, `- committedCommit: ${entry.committedCommit ? mdInline(entry.committedCommit) : "(none)"}`, `- checkedOutCommit: ${entry.checkedOutCommit ? mdInline(entry.checkedOutCommit) : "(none)"}`, `- dirty: ${entry.dirty}`);
|
|
37
78
|
if (entry.staleNoteIds.length > 0) lines.push(`- staleNoteIds: ${entry.staleNoteIds.map(mdInline).join(", ")}`);
|
|
38
79
|
}
|
|
39
80
|
if (r.repos.length === 0) lines.push("(none)");
|
|
@@ -66,6 +107,28 @@ const renderMarkdown = (data) => {
|
|
|
66
107
|
if (Object.keys(r.repos).length === 0) lines.push("(none)");
|
|
67
108
|
return lines.join("\n");
|
|
68
109
|
}
|
|
110
|
+
case "drift": {
|
|
111
|
+
const r = data.report;
|
|
112
|
+
const lines = [
|
|
113
|
+
`# repos drift`,
|
|
114
|
+
``,
|
|
115
|
+
`clean: ${r.clean}`,
|
|
116
|
+
``,
|
|
117
|
+
`| name | kind | detail |`,
|
|
118
|
+
`| --- | --- | --- |`
|
|
119
|
+
];
|
|
120
|
+
for (const d of r.drifts) lines.push(`| ${mdInline(d.name)} | ${mdInline(d.kind)} | ${mdInline(d.detail)} |`);
|
|
121
|
+
if (r.drifts.length === 0) lines.push(``, `(none)`);
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
case "gitmodules": {
|
|
125
|
+
const lines = [`# repos gitmodules`, ``];
|
|
126
|
+
if (data.parseError) lines.push(`parse error: ${mdInline(data.parseError)}`, ``);
|
|
127
|
+
lines.push(`| name | path | url | branch | shallow |`, `| --- | --- | --- | --- | --- |`);
|
|
128
|
+
for (const entry of data.entries) lines.push(`| ${mdInline(entry.name)} | ${mdInline(entry.path)} | ${mdInline(entry.url)} | ${entry.branch === void 0 ? "(none)" : mdInline(entry.branch)} | ${entry.shallow === void 0 ? "(unset)" : entry.shallow} |`);
|
|
129
|
+
if (data.entries.length === 0) lines.push(``, `(none)`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
69
132
|
}
|
|
70
133
|
};
|
|
71
134
|
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
@@ -88,8 +151,41 @@ const reposInspect = (args, fallbackCwd) => Effect.gen(function* () {
|
|
|
88
151
|
mode: "config",
|
|
89
152
|
result: yield* (yield* Repos.ReposConfigStore).read(root)
|
|
90
153
|
};
|
|
154
|
+
case "drift": return {
|
|
155
|
+
mode: "drift",
|
|
156
|
+
report: yield* (yield* Repos.ReposDrift).check(root)
|
|
157
|
+
};
|
|
158
|
+
case "gitmodules": {
|
|
159
|
+
const fs = yield* FileSystem.FileSystem;
|
|
160
|
+
const gitmodulesPath = (yield* Path.Path).join(root, ".gitmodules");
|
|
161
|
+
const text = yield* fs.readFileString(gitmodulesPath).pipe(Effect.map(Option.some), Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(new Repos.GitSubmoduleError({
|
|
162
|
+
command: "read .gitmodules",
|
|
163
|
+
cwd: gitmodulesPath,
|
|
164
|
+
reason: error.message
|
|
165
|
+
}))));
|
|
166
|
+
if (Option.isNone(text)) return {
|
|
167
|
+
mode: "gitmodules",
|
|
168
|
+
entries: []
|
|
169
|
+
};
|
|
170
|
+
const parsed = Gitmodules.parseResult(text.value);
|
|
171
|
+
if (Result.isFailure(parsed)) return {
|
|
172
|
+
mode: "gitmodules",
|
|
173
|
+
entries: [],
|
|
174
|
+
parseError: parsed.failure.message
|
|
175
|
+
};
|
|
176
|
+
const decoded = Schema.decodeUnknownResult(Schema.Array(GitmodulesEntrySchema))(parsed.success.entries);
|
|
177
|
+
if (Result.isFailure(decoded)) return {
|
|
178
|
+
mode: "gitmodules",
|
|
179
|
+
entries: [],
|
|
180
|
+
parseError: `unexpected .gitmodules entry shape: ${decoded.failure.message}`
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
mode: "gitmodules",
|
|
184
|
+
entries: decoded.success
|
|
185
|
+
};
|
|
186
|
+
}
|
|
91
187
|
}
|
|
92
188
|
});
|
|
93
189
|
|
|
94
190
|
//#endregion
|
|
95
|
-
export { ReposConfigResult, ReposInspectAsMarkdown, ReposInspectResult, ReposStatusResult, reposInspect };
|
|
191
|
+
export { ReposConfigResult, ReposDriftResult, ReposGitmodulesResult, ReposInspectAsMarkdown, ReposInspectResult, ReposStatusResult, reposInspect };
|
package/tools/repos-manage.js
CHANGED
|
@@ -40,12 +40,24 @@ const NoteRequest = Schema.TaggedStruct("note", {
|
|
|
40
40
|
if (request.op === "promote" && (request.id === void 0 || request.into === void 0)) return "note op \"promote\" requires both `id` and `into`";
|
|
41
41
|
return true;
|
|
42
42
|
}));
|
|
43
|
+
/** `remove` requires only `name`. */
|
|
44
|
+
const RemoveRequest = Schema.TaggedStruct("remove", { name: Schema.String });
|
|
45
|
+
/** `rename` requires `name` (the old name) and `newName`. */
|
|
46
|
+
const RenameRequest = Schema.TaggedStruct("rename", {
|
|
47
|
+
name: Schema.String,
|
|
48
|
+
newName: Schema.String
|
|
49
|
+
});
|
|
50
|
+
/** `restore`'s `names` is optional and repeatable, mirroring `add`'s `sparse`; omitted means "every dirty entry". */
|
|
51
|
+
const RestoreRequest = Schema.TaggedStruct("restore", { names: Schema.optional(Schema.Array(Schema.String)) });
|
|
43
52
|
/** Internal tagged-union request the flat wire args decode into. */
|
|
44
53
|
const ReposManageRequest = Schema.Union([
|
|
45
54
|
SyncRequest,
|
|
46
55
|
PinRequest,
|
|
47
56
|
AddRequest,
|
|
48
|
-
NoteRequest
|
|
57
|
+
NoteRequest,
|
|
58
|
+
RemoveRequest,
|
|
59
|
+
RenameRequest,
|
|
60
|
+
RestoreRequest
|
|
49
61
|
]);
|
|
50
62
|
/** `sync` result variant. */
|
|
51
63
|
const ReposManageSyncResult = Schema.Struct({
|
|
@@ -67,16 +79,34 @@ const ReposManageNoteResult = Schema.Struct({
|
|
|
67
79
|
action: Schema.Literal("note"),
|
|
68
80
|
result: Repos.ReposNoteResult
|
|
69
81
|
}).annotate({ identifier: "ReposManageNoteResult" });
|
|
82
|
+
/** `remove` result variant. */
|
|
83
|
+
const ReposManageRemoveResult = Schema.Struct({
|
|
84
|
+
action: Schema.Literal("remove"),
|
|
85
|
+
result: Repos.ReposRemoveResult
|
|
86
|
+
}).annotate({ identifier: "ReposManageRemoveResult" });
|
|
87
|
+
/** `rename` result variant. */
|
|
88
|
+
const ReposManageRenameResult = Schema.Struct({
|
|
89
|
+
action: Schema.Literal("rename"),
|
|
90
|
+
result: Repos.ReposRenameResult
|
|
91
|
+
}).annotate({ identifier: "ReposManageRenameResult" });
|
|
92
|
+
/** `restore` result variant. */
|
|
93
|
+
const ReposManageRestoreResult = Schema.Struct({
|
|
94
|
+
action: Schema.Literal("restore"),
|
|
95
|
+
result: Repos.ReposRestoreResult
|
|
96
|
+
}).annotate({ identifier: "ReposManageRestoreResult" });
|
|
70
97
|
/** The `repos_manage` tool result — a discriminated union keyed by `action`. */
|
|
71
98
|
const ReposManageResult = Schema.Union([
|
|
72
99
|
ReposManageSyncResult,
|
|
73
100
|
ReposManagePinResult,
|
|
74
101
|
ReposManageAddResult,
|
|
75
|
-
ReposManageNoteResult
|
|
102
|
+
ReposManageNoteResult,
|
|
103
|
+
ReposManageRemoveResult,
|
|
104
|
+
ReposManageRenameResult,
|
|
105
|
+
ReposManageRestoreResult
|
|
76
106
|
]).annotate({
|
|
77
107
|
identifier: "ReposManageResult",
|
|
78
108
|
title: "repos_manage result",
|
|
79
|
-
description: "Result of a mutating repos action: sync, pin, add, or
|
|
109
|
+
description: "Result of a mutating repos action: sync, pin, add, note, remove, rename, or restore."
|
|
80
110
|
});
|
|
81
111
|
/** Render the structured result as a markdown transcript. */
|
|
82
112
|
const renderMarkdown = (data) => {
|
|
@@ -93,7 +123,11 @@ const renderMarkdown = (data) => {
|
|
|
93
123
|
``,
|
|
94
124
|
...section("Up to date", r.upToDate),
|
|
95
125
|
``,
|
|
96
|
-
...section("Cleared locks", r.clearedLocks)
|
|
126
|
+
...section("Cleared locks", r.clearedLocks),
|
|
127
|
+
``,
|
|
128
|
+
...section("URL synced", r.urlSynced),
|
|
129
|
+
``,
|
|
130
|
+
...section("Registered", r.registered)
|
|
97
131
|
].join("\n");
|
|
98
132
|
}
|
|
99
133
|
case "pin": {
|
|
@@ -136,6 +170,54 @@ const renderMarkdown = (data) => {
|
|
|
136
170
|
`noteCount: ${r.noteCount}`
|
|
137
171
|
].join("\n");
|
|
138
172
|
}
|
|
173
|
+
case "remove": {
|
|
174
|
+
const r = data.result;
|
|
175
|
+
const lines = [
|
|
176
|
+
`# repos remove — ${mdInline(r.name)}`,
|
|
177
|
+
``,
|
|
178
|
+
`path: ${mdInline(r.path)}`,
|
|
179
|
+
``,
|
|
180
|
+
`## Commit message`,
|
|
181
|
+
``,
|
|
182
|
+
mdInline(r.commitMessage),
|
|
183
|
+
``,
|
|
184
|
+
`## Removed notes`,
|
|
185
|
+
``
|
|
186
|
+
];
|
|
187
|
+
if (r.removedNotes.length > 0) lines.push(`Promote any durable ones elsewhere before committing:`, ...r.removedNotes.map((note) => `- ${mdInline(note.id)} (${mdInline(note.ref)}): ${mdInline(note.note)}`));
|
|
188
|
+
else lines.push("(none)");
|
|
189
|
+
lines.push(``, `REVIEW AND COMMIT: the manifest, .gitmodules, and gitlink removal are already staged — review and commit using the message above.`);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
case "rename": {
|
|
193
|
+
const r = data.result;
|
|
194
|
+
return [
|
|
195
|
+
`# repos rename — ${mdInline(r.oldName)} → ${mdInline(r.newName)}`,
|
|
196
|
+
``,
|
|
197
|
+
`path: ${mdInline(r.path)}`,
|
|
198
|
+
``,
|
|
199
|
+
`## Commit message`,
|
|
200
|
+
``,
|
|
201
|
+
mdInline(r.commitMessage),
|
|
202
|
+
``,
|
|
203
|
+
`REVIEW AND COMMIT: the moved worktree, .gitmodules section, and manifest key are already staged — review and commit using the message above.`
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
206
|
+
case "restore": {
|
|
207
|
+
const r = data.result;
|
|
208
|
+
const lines = [
|
|
209
|
+
`# repos restore`,
|
|
210
|
+
``,
|
|
211
|
+
`## Restored`,
|
|
212
|
+
``
|
|
213
|
+
];
|
|
214
|
+
if (r.restored.length > 0) lines.push(`DESTRUCTIVE: any uncommitted worktree edits and untracked files in these repos were discarded — the working tree was hard-reset to the commit below and sparse paths re-applied.`, ``, ...r.restored.map((entry) => `- ${mdInline(entry.name)} → ${mdInline(entry.commit)}`));
|
|
215
|
+
else lines.push("(none)");
|
|
216
|
+
lines.push(``, `## Skipped (clean)`, ``);
|
|
217
|
+
if (r.skippedClean.length > 0) lines.push(...r.skippedClean.map((name) => `- ${mdInline(name)}`));
|
|
218
|
+
else lines.push("(none)");
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
139
221
|
}
|
|
140
222
|
};
|
|
141
223
|
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
@@ -192,8 +274,20 @@ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
|
|
|
192
274
|
result: yield* manager.note(root, request.name, noteOp)
|
|
193
275
|
};
|
|
194
276
|
}
|
|
277
|
+
case "remove": return {
|
|
278
|
+
action: "remove",
|
|
279
|
+
result: yield* manager.remove(root, request.name)
|
|
280
|
+
};
|
|
281
|
+
case "rename": return {
|
|
282
|
+
action: "rename",
|
|
283
|
+
result: yield* manager.rename(root, request.name, request.newName)
|
|
284
|
+
};
|
|
285
|
+
case "restore": return {
|
|
286
|
+
action: "restore",
|
|
287
|
+
result: yield* manager.restore(root, request.names)
|
|
288
|
+
};
|
|
195
289
|
}
|
|
196
290
|
});
|
|
197
291
|
|
|
198
292
|
//#endregion
|
|
199
|
-
export { ReposManageAddResult, ReposManageAsMarkdown, ReposManageNoteResult, ReposManagePinResult, ReposManageResult, ReposManageSyncResult, reposManage };
|
|
293
|
+
export { ReposManageAddResult, ReposManageAsMarkdown, ReposManageNoteResult, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, reposManage };
|