@luanpdd/kit-mcp 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -1
- package/README.md +30 -14
- package/kit/commands/adicionar-backlog.md +1 -1
- package/kit/commands/adicionar-fase.md +1 -1
- package/kit/commands/adicionar-tarefa.md +1 -1
- package/kit/commands/concluir-marco.md +1 -1
- package/kit/commands/definir-perfil.md +1 -1
- package/kit/commands/depurar.md +1 -1
- package/kit/commands/fio.md +1 -1
- package/kit/commands/inserir-fase.md +1 -1
- package/package.json +5 -2
- package/src/core/kit.js +4 -1
- package/src/core/registry.js +9 -5
- package/src/core/reverse-sync.js +93 -4
- package/src/core/sync.js +67 -8
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,67 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.0] - 2026-05-03
|
|
10
|
+
|
|
11
|
+
**First stable release.** kit-mcp now commits to backwards compatibility on the surfaces listed under "Stable API" below; breaking changes there require a 2.0.0 bump.
|
|
12
|
+
|
|
13
|
+
### Added — Phase 1: Tooling debt
|
|
14
|
+
- `.github/dependabot.yml` — weekly grouped npm + github-actions updates.
|
|
15
|
+
- GitHub Release object created for v0.5.0 (was stuck on v0.2.0 "cleanup" as Latest).
|
|
16
|
+
- `.github/workflows/publish.yml` now creates a GitHub Release object automatically on every `v*` tag push, with notes extracted from this CHANGELOG. Closes the gap permanently.
|
|
17
|
+
|
|
18
|
+
### Fixed — Phase 2: Slash-command parser
|
|
19
|
+
- `src/core/sync.js` — `renderReference` reorders the stub body so the first non-blank line is the H1 + description blockquote, not the `<!-- kit-mcp:reference -->` marker. Strict downstream parsers (notably Claude Desktop's skill listing) now surface the real description.
|
|
20
|
+
- `src/core/kit.js` — `firstNonEmptyLine` skips lines starting with `<!--` as a defensive fallback when the canonical has no frontmatter description.
|
|
21
|
+
- `kit/commands/*` — 8 commands (`adicionar-backlog`, `adicionar-fase`, `adicionar-tarefa`, `concluir-marco`, `definir-perfil`, `depurar`, `fio`, `inserir-fase`) had unquoted angle-bracket `argument-hint` values that strict YAML parsers misinterpreted as flow-style flags. Now consistently quoted.
|
|
22
|
+
|
|
23
|
+
### Added — Phase 3: Reverse-sync for mirror-tree caps
|
|
24
|
+
- `detectReverse` now walks `.claude/framework/` and `.claude/hooks/` and reports any byte-for-byte difference vs `kit/<source>/<rel>`. The `.kit-mcp-managed` marker is automatically excluded from candidates.
|
|
25
|
+
- `applyReverse` adds `applyMirrorTreeOne` for `framework`/`hooks` candidates: `skip`, `overwrite`, `merge` (degenerates to overwrite — no frontmatter to preserve), `rename` (writes to `kit/<source>/<rel>.from-<tag>.<ext>` preserving the original).
|
|
26
|
+
- `--only framework/<rel>` / `--only hooks/<file>` filters narrow apply to one file.
|
|
27
|
+
- README "kit reverse-sync" section updated with the new examples.
|
|
28
|
+
|
|
29
|
+
### Added — Phase 4: Test infrastructure
|
|
30
|
+
- `node:test`-based runner — zero dependencies. `test/run.mjs` walks for `*.test.js` files (works on Node 20+ where `--test` glob support is partial).
|
|
31
|
+
- 37 unit tests across `kit`, `sync`, `reverse-sync`, `gates`, `gate-runner`, `registry`.
|
|
32
|
+
- 5 integration tests spawning `bin/cli.js` end-to-end (incl. MCP server boot smoke).
|
|
33
|
+
- `test/fixtures/sample-kit/` minimal fixture (1 of each kind + framework template + hook + frontmatter-less command for fallback test).
|
|
34
|
+
- CI runs `npm test` + `npm run test:integration` before existing smoke + MCP boot, on Ubuntu / macOS / Windows × Node 20 / 22 (6/6 combinations).
|
|
35
|
+
- `package.json` scripts: `test`, `test:integration`, `test:all`.
|
|
36
|
+
|
|
37
|
+
### Stable API (commitments locked at 1.0.0)
|
|
38
|
+
|
|
39
|
+
The following surfaces are covered by SemVer — breaking changes require a 2.0.0 release:
|
|
40
|
+
|
|
41
|
+
- **`src/core/registry.js` TARGETS table format.** Adding capabilities, IDEs, or new modes is non-breaking. Renaming or removing existing capability keys (`rules`, `agents`, `commands`, `skills`, `framework`, `hooks`, `mcpConfig`) is breaking.
|
|
42
|
+
- **MCP tool action signatures.** Tool names (`kit`, `sync`, `reverse-sync`, `gates`, `forensics`, `install`) and their action-dispatch contracts are stable. New actions are non-breaking; renaming or removing existing actions is breaking.
|
|
43
|
+
- **CLI subcommand surface.** Top-level commands (`kit`, `sync`, `reverse-sync`, `gates`, `forensics`, `install`) and their action sub-commands are stable. New flags are non-breaking; renaming or removing existing ones is breaking.
|
|
44
|
+
- **`src/core/*.js` named exports.** Functions consumed programmatically (`listKit`, `searchKit`, `findItem`, `resolveKitRoot`, `BUNDLED_KIT_ROOT`, `syncTo`, `statusOf`, `removeFrom`, `detectReverse`, `applyReverse`, `listGates`, `getGate`, `gatesForStage`, `runGate`, `listTargets`, `getTarget`, `TARGETS`) keep their signatures. Adding new exports is non-breaking; signature changes are breaking.
|
|
45
|
+
- **Stub format.** Files written by sync `--mode reference` keep the `<!-- kit-mcp:reference -->` marker somewhere in the body so `sync remove` and `reverse-sync detect` continue to identify them. Position within the body may change; presence is the contract.
|
|
46
|
+
- **`.kit-mcp-managed` marker semantics.** Mirror-tree directories (`framework/`, `hooks/`) are managed only when the marker is present at the root. Without it, `sync remove` never deletes the tree.
|
|
47
|
+
|
|
48
|
+
### Migration
|
|
49
|
+
|
|
50
|
+
No code changes required for users on 0.5.0 — `npm install @luanpdd/kit-mcp@latest` brings in 1.0.0 with the same behavior plus the parser fixes, reverse-sync expansions, and test coverage.
|
|
51
|
+
|
|
52
|
+
If you were on 0.4.0 (deprecated) or earlier, upgrade to skip the import-time crash and missing-framework regression entirely.
|
|
53
|
+
|
|
54
|
+
## [0.5.0] - 2026-05-03
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **Mirror-tree sync for `framework` and `hooks`.** `kit/framework/` (124 files: workflows, templates, references, libs) and `kit/hooks/` (5 files) are now projected into `.claude/framework/` and `.claude/hooks/` on every `sync install claude-code`. Without this, the bundled slash-commands like `/novo-marco` were broken-by-design — they referenced `@./.claude/framework/workflows/new-milestone.md` and similar paths that never existed in the destination project. Now they resolve correctly end-to-end.
|
|
58
|
+
- New `mode: 'mirror-tree'` capability spec in `src/core/registry.js`. Each mirror-tree entry has a `source` (relative path inside `kit/`) and a `path` (destination path in the target project).
|
|
59
|
+
- A `.kit-mcp-managed` marker file is written at the root of each managed tree so `kit sync remove` can recursively clean up the directory **only** when the marker is present. Trees you authored yourself (without the marker) are never touched.
|
|
60
|
+
- CI smoke test asserts `.claude/framework/workflows/new-milestone.md`, `.claude/framework/templates/project.md`, and `.claude/hooks/workflow-guard.js` are projected, and that `sync remove` cleans them up.
|
|
61
|
+
- New CI safety test: `sync remove` against a `.claude/framework/` directory with no marker preserves user content.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- `statusOf` now reports `framework` and `hooks` capability paths.
|
|
65
|
+
- README capability matrix gained two columns (`framework`, `hooks`) and a paragraph explaining the mirror-tree semantics.
|
|
66
|
+
|
|
67
|
+
### Migration
|
|
68
|
+
No action needed — `npx -y @luanpdd/kit-mcp@latest sync install claude-code --project-root .` projects the new directories automatically. If you had a manually-created `.claude/framework/` or `.claude/hooks/`, kit-mcp will overwrite individual files but won't delete user files; `sync remove` continues to leave them alone.
|
|
69
|
+
|
|
9
70
|
## [0.4.1] - 2026-05-03
|
|
10
71
|
|
|
11
72
|
### Fixed
|
|
@@ -158,7 +219,9 @@ npx -y @luanpdd/kit-mcp sync install claude-code --project-root .
|
|
|
158
219
|
- CLI mirror of all MCP tools.
|
|
159
220
|
- `install` command that registers kit-mcp into an IDE's MCP config (JSON for Claude/Cursor/Gemini/Windsurf, TOML for Codex).
|
|
160
221
|
|
|
161
|
-
[Unreleased]: https://github.com/luanpdd/kit-mcp/compare/
|
|
222
|
+
[Unreleased]: https://github.com/luanpdd/kit-mcp/compare/v1.0.0...HEAD
|
|
223
|
+
[1.0.0]: https://github.com/luanpdd/kit-mcp/compare/v0.5.0...v1.0.0
|
|
224
|
+
[0.5.0]: https://github.com/luanpdd/kit-mcp/compare/v0.4.1...v0.5.0
|
|
162
225
|
[0.4.1]: https://github.com/luanpdd/kit-mcp/compare/v0.4.0...v0.4.1
|
|
163
226
|
[0.4.0]: https://github.com/luanpdd/kit-mcp/compare/v0.3.0...v0.4.0
|
|
164
227
|
[0.3.0]: https://github.com/luanpdd/kit-mcp/compare/v0.2.1...v0.3.0
|
package/README.md
CHANGED
|
@@ -159,19 +159,21 @@ kit sync watch --all # watch + auto-detect every IDE al
|
|
|
159
159
|
|
|
160
160
|
**Per-IDE projection** — what each target receives:
|
|
161
161
|
|
|
162
|
-
| IDE | rules → | agents → | commands → | skills → |
|
|
163
|
-
|
|
164
|
-
| Claude Code | `CLAUDE.md` | `.claude/agents/*.md` | `.claude/commands/*.md` | `.claude/skills/*/` |
|
|
165
|
-
| Cursor | `.cursor/rules/*.mdc` | `.cursor/agents/*.md` | — | — |
|
|
166
|
-
| Codex | `AGENTS.md` | — | — | `.codex/skills/*/` |
|
|
167
|
-
| Gemini CLI | `GEMINI.md` | — | — | `.gemini/skills/*/` |
|
|
168
|
-
| Copilot | `.github/copilot-instructions.md` | `.github/agents/*.agent` | — | `.github/skills/*/` |
|
|
169
|
-
| Windsurf | `.windsurf/rules/*.md` | `.windsurf/agents/*.md` | — | `.windsurf/skills/*/` |
|
|
170
|
-
| Antigravity | `.agents/rules/*.md` | `.agents/agents/*.md` | — | `.agents/workflows/*/` |
|
|
171
|
-
| Trae | `.trae/rules/*.md` | `.trae/agents/*.md` | — | — |
|
|
162
|
+
| IDE | rules → | agents → | commands → | skills → | framework → | hooks → |
|
|
163
|
+
|---|---|---|---|---|---|---|
|
|
164
|
+
| Claude Code | `CLAUDE.md` | `.claude/agents/*.md` | `.claude/commands/*.md` | `.claude/skills/*/` | `.claude/framework/**` | `.claude/hooks/**` |
|
|
165
|
+
| Cursor | `.cursor/rules/*.mdc` | `.cursor/agents/*.md` | — | — | — | — |
|
|
166
|
+
| Codex | `AGENTS.md` | — | — | `.codex/skills/*/` | — | — |
|
|
167
|
+
| Gemini CLI | `GEMINI.md` | — | — | `.gemini/skills/*/` | — | — |
|
|
168
|
+
| Copilot | `.github/copilot-instructions.md` | `.github/agents/*.agent` | — | `.github/skills/*/` | — | — |
|
|
169
|
+
| Windsurf | `.windsurf/rules/*.md` | `.windsurf/agents/*.md` | — | `.windsurf/skills/*/` | — | — |
|
|
170
|
+
| Antigravity | `.agents/rules/*.md` | `.agents/agents/*.md` | — | `.agents/workflows/*/` | — | — |
|
|
171
|
+
| Trae | `.trae/rules/*.md` | `.trae/agents/*.md` | — | — | — | — |
|
|
172
172
|
|
|
173
173
|
A capability marked `—` is not supported by that IDE. Adding a new IDE = one entry in [`src/core/registry.js`](src/core/registry.js).
|
|
174
174
|
|
|
175
|
+
**About `framework` and `hooks`:** these are *mirror-tree* capabilities — the entire `kit/framework/` and `kit/hooks/` subtrees are copied verbatim into `.claude/framework/` and `.claude/hooks/`. They're needed by the bundled workflow because slash-commands like `/novo-marco` reference framework files via paths like `@./.claude/framework/workflows/new-milestone.md`. A `.kit-mcp-managed` marker is written at the root of each managed tree so `kit sync remove` can clean up safely without touching directories you authored yourself.
|
|
176
|
+
|
|
175
177
|
### `kit install ...` — register kit-mcp into an IDE's MCP config
|
|
176
178
|
|
|
177
179
|
```bash
|
|
@@ -192,16 +194,19 @@ kit install write claude-code --scope user --via global # assumes `npm ins
|
|
|
192
194
|
|
|
193
195
|
### `kit reverse-sync ...` — bring IDE edits back to the canonical kit
|
|
194
196
|
|
|
195
|
-
If you edited an agent/command/skill **directly inside the IDE's folder** (`.claude/agents/foo.md`, `.
|
|
197
|
+
If you edited an agent/command/skill/framework/hook **directly inside the IDE's folder** (`.claude/agents/foo.md`, `.claude/framework/workflows/bar.md`, `.claude/hooks/baz.js`, …) instead of in your kit, this brings those edits back so the canonical absorbs them.
|
|
196
198
|
|
|
197
199
|
```bash
|
|
198
200
|
kit reverse-sync detect claude-code --project-root .
|
|
199
201
|
kit reverse-sync apply claude-code --project-root . --strategy merge --dry-run
|
|
200
202
|
kit reverse-sync apply claude-code --project-root . --strategy merge
|
|
201
203
|
kit reverse-sync apply claude-code --project-root . --strategy overwrite --only agent/foo
|
|
204
|
+
kit reverse-sync apply claude-code --project-root . --strategy overwrite --only framework/workflows/new-milestone.md
|
|
202
205
|
```
|
|
203
206
|
|
|
204
|
-
**Strategies:** `skip` (list-only), `merge` (canonical frontmatter + edited body), `overwrite`, `rename` (preserve both as `-from-{ide}.md`).
|
|
207
|
+
**Strategies:** `skip` (list-only), `merge` (canonical frontmatter + edited body — for agents/commands/skills), `overwrite`, `rename` (preserve both as `-from-{ide}.md`).
|
|
208
|
+
|
|
209
|
+
**Mirror-tree caps (`framework`, `hooks`):** files have no frontmatter, so `merge` degenerates to `overwrite` (with a note). The `.kit-mcp-managed` marker is automatically excluded from candidates. Filter individual files with `--only framework/<rel>` or `--only hooks/<file>`.
|
|
205
210
|
|
|
206
211
|
### `kit gates ...` — reusable workflow gates
|
|
207
212
|
|
|
@@ -467,16 +472,27 @@ PRs welcome.
|
|
|
467
472
|
|
|
468
473
|
---
|
|
469
474
|
|
|
470
|
-
##
|
|
475
|
+
## Tests
|
|
476
|
+
|
|
477
|
+
Built on `node:test` (zero dependencies). Two suites:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
npm test # unit — kit parser, sync (all modes), reverse-sync, gates, registry
|
|
481
|
+
npm run test:integration # integration — spawns bin/cli.js end-to-end on a temp project
|
|
482
|
+
npm run test:all # both
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Plus the original quick smokes:
|
|
471
486
|
|
|
472
487
|
```bash
|
|
473
488
|
node bin/cli.js kit list-agents | head -5 # 19 bundled agents
|
|
474
489
|
node bin/cli.js sync targets # 8 IDEs
|
|
475
490
|
node bin/cli.js gates list # 5 gates
|
|
476
491
|
node bin/cli.js install dry-run claude-code --via npx
|
|
477
|
-
node bin/mcp.js < /dev/null & sleep 1; kill %1 # MCP server boots and waits on stdio
|
|
478
492
|
```
|
|
479
493
|
|
|
494
|
+
CI runs unit + integration + smoke + MCP boot on Ubuntu / macOS / Windows × Node 20 / 22 on every push and PR.
|
|
495
|
+
|
|
480
496
|
---
|
|
481
497
|
|
|
482
498
|
## License
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: definir-perfil
|
|
3
3
|
description: Altera o perfil de modelo para os agentes framework (quality/balanced/budget/inherit)
|
|
4
|
-
argument-hint: <perfil (quality|balanced|budget|inherit)>
|
|
4
|
+
argument-hint: "<perfil (quality|balanced|budget|inherit)>"
|
|
5
5
|
model: haiku
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Bash
|
package/kit/commands/depurar.md
CHANGED
package/kit/commands/fio.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"start": "node bin/mcp.js",
|
|
43
43
|
"cli": "node bin/cli.js",
|
|
44
|
-
"smoke": "node bin/cli.js kit list-agents | head -5"
|
|
44
|
+
"smoke": "node bin/cli.js kit list-agents | head -5",
|
|
45
|
+
"test": "node test/run.mjs test/unit",
|
|
46
|
+
"test:integration": "node test/run.mjs test/integration",
|
|
47
|
+
"test:all": "node test/run.mjs test"
|
|
45
48
|
},
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
package/src/core/kit.js
CHANGED
|
@@ -135,7 +135,10 @@ function parseLooseYaml(text) {
|
|
|
135
135
|
function firstNonEmptyLine(body) {
|
|
136
136
|
for (const line of body.split(/\r?\n/)) {
|
|
137
137
|
const t = line.trim();
|
|
138
|
-
if (
|
|
138
|
+
if (!t) continue; // blank
|
|
139
|
+
if (t.startsWith('#')) continue; // markdown heading
|
|
140
|
+
if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
|
|
141
|
+
return t.slice(0, 200);
|
|
139
142
|
}
|
|
140
143
|
return '';
|
|
141
144
|
}
|
package/src/core/registry.js
CHANGED
|
@@ -20,6 +20,8 @@ export const TARGETS = {
|
|
|
20
20
|
agents: { path: '.claude/agents/', mode: 'multi', extension: '.md' },
|
|
21
21
|
commands: { path: '.claude/commands/', mode: 'multi', extension: '.md' },
|
|
22
22
|
skills: { path: '.claude/skills/', mode: 'multi-dir' },
|
|
23
|
+
framework:{ path: '.claude/framework/', mode: 'mirror-tree', source: 'framework' },
|
|
24
|
+
hooks: { path: '.claude/hooks/', mode: 'mirror-tree', source: 'hooks' },
|
|
23
25
|
mcpConfig:{ path: '.mcp.json', strategy: 'merge-mcpServers-json',
|
|
24
26
|
userPath: '~/.claude.json', userKey: 'mcpServers' },
|
|
25
27
|
},
|
|
@@ -92,11 +94,13 @@ export function listTargets() {
|
|
|
92
94
|
id,
|
|
93
95
|
label: t.label,
|
|
94
96
|
capabilities: {
|
|
95
|
-
rules:
|
|
96
|
-
agents:
|
|
97
|
-
commands:
|
|
98
|
-
skills:
|
|
99
|
-
|
|
97
|
+
rules: !!t.rules,
|
|
98
|
+
agents: !!t.agents,
|
|
99
|
+
commands: !!t.commands,
|
|
100
|
+
skills: !!t.skills,
|
|
101
|
+
framework: !!t.framework,
|
|
102
|
+
hooks: !!t.hooks,
|
|
103
|
+
mcpConfig: !!t.mcpConfig,
|
|
100
104
|
},
|
|
101
105
|
}));
|
|
102
106
|
}
|
package/src/core/reverse-sync.js
CHANGED
|
@@ -35,6 +35,11 @@ export async function detectReverse(targetId, opts = {}) {
|
|
|
35
35
|
if (target.agents) await scanCapability(candidates, 'agent', target.agents, projectRoot, kit.agents, kitRoot);
|
|
36
36
|
if (target.commands) await scanCapability(candidates, 'command', target.commands, projectRoot, kit.commands, kitRoot);
|
|
37
37
|
if (target.skills) await scanSkills (candidates, target.skills, projectRoot, [...kit.skills, ...kit.skillsExtras], kitRoot);
|
|
38
|
+
for (const cap of ['framework', 'hooks']) {
|
|
39
|
+
const spec = target[cap];
|
|
40
|
+
if (!spec || spec.mode !== 'mirror-tree') continue;
|
|
41
|
+
await scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot);
|
|
42
|
+
}
|
|
38
43
|
|
|
39
44
|
return { target: targetId, projectRoot, kitRoot, candidates };
|
|
40
45
|
}
|
|
@@ -117,6 +122,51 @@ async function scanSkills(candidates, capCfg, projectRoot, kitSkills, kitRoot) {
|
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
async function scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot) {
|
|
126
|
+
const dstRoot = path.join(projectRoot, spec.path);
|
|
127
|
+
const srcRoot = path.join(kitRoot, spec.source);
|
|
128
|
+
const files = await walkRel(dstRoot);
|
|
129
|
+
for (const rel of files) {
|
|
130
|
+
if (rel === '.kit-mcp-managed' || path.basename(rel) === '.kit-mcp-managed') continue;
|
|
131
|
+
const dstPath = path.join(dstRoot, rel);
|
|
132
|
+
const srcPath = path.join(srcRoot, rel);
|
|
133
|
+
let dstBuf, srcBuf;
|
|
134
|
+
try { dstBuf = await fs.readFile(dstPath); } catch { continue; }
|
|
135
|
+
try { srcBuf = await fs.readFile(srcPath); } catch { srcBuf = null; }
|
|
136
|
+
if (!srcBuf) {
|
|
137
|
+
candidates.push({
|
|
138
|
+
kind: cap, name: rel, target: spec.path, destPath: dstPath, kitPath: srcPath,
|
|
139
|
+
reason: 'new-in-ide',
|
|
140
|
+
diffSummary: `+${dstBuf.length} bytes (no kit source)`,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (dstBuf.equals(srcBuf)) continue;
|
|
145
|
+
candidates.push({
|
|
146
|
+
kind: cap, name: rel, target: spec.path, destPath: dstPath, kitPath: srcPath,
|
|
147
|
+
reason: 'modified-in-ide',
|
|
148
|
+
diffSummary: `${dstBuf.length} bytes vs ${srcBuf.length} canonical (${dstBuf.length - srcBuf.length >= 0 ? '+' : ''}${dstBuf.length - srcBuf.length})`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function walkRel(root) {
|
|
154
|
+
const out = [];
|
|
155
|
+
async function visit(current, prefix) {
|
|
156
|
+
let entries;
|
|
157
|
+
try { entries = await fs.readdir(current, { withFileTypes: true }); }
|
|
158
|
+
catch { return; }
|
|
159
|
+
for (const e of entries) {
|
|
160
|
+
const abs = path.join(current, e.name);
|
|
161
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
162
|
+
if (e.isDirectory()) await visit(abs, rel);
|
|
163
|
+
else if (e.isFile()) out.push(rel);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await visit(root, '');
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
// --- apply ---
|
|
121
171
|
|
|
122
172
|
export async function applyReverse(targetId, opts = {}) {
|
|
@@ -139,6 +189,13 @@ export async function applyReverse(targetId, opts = {}) {
|
|
|
139
189
|
|
|
140
190
|
async function applyOne(c, strategy, opts) {
|
|
141
191
|
const dryRun = !!opts.dryRun;
|
|
192
|
+
const isMirrorTree = c.kind === 'framework' || c.kind === 'hooks';
|
|
193
|
+
|
|
194
|
+
// Mirror-tree files don't have stub boilerplate — copy bytes verbatim.
|
|
195
|
+
if (isMirrorTree) {
|
|
196
|
+
return applyMirrorTreeOne(c, strategy, dryRun);
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
const destContent = await fs.readFile(c.destPath, 'utf8');
|
|
143
200
|
const stripped = stripStubBoilerplate(destContent);
|
|
144
201
|
|
|
@@ -147,7 +204,6 @@ async function applyOne(c, strategy, opts) {
|
|
|
147
204
|
return 'skipped';
|
|
148
205
|
|
|
149
206
|
case 'overwrite': {
|
|
150
|
-
// Replace canonical with the destination content (stripped of stub boilerplate)
|
|
151
207
|
if (!dryRun) {
|
|
152
208
|
await fs.mkdir(path.dirname(c.kitPath), { recursive: true });
|
|
153
209
|
await fs.writeFile(c.kitPath, stripped, 'utf8');
|
|
@@ -156,8 +212,6 @@ async function applyOne(c, strategy, opts) {
|
|
|
156
212
|
}
|
|
157
213
|
|
|
158
214
|
case 'merge': {
|
|
159
|
-
// Frontmatter from canonical (if present), body from destination.
|
|
160
|
-
// If canonical doesn't exist (new-in-ide), this degenerates to overwrite.
|
|
161
215
|
let merged = stripped;
|
|
162
216
|
if (c.reason === 'modified-in-ide') {
|
|
163
217
|
try {
|
|
@@ -173,7 +227,6 @@ async function applyOne(c, strategy, opts) {
|
|
|
173
227
|
}
|
|
174
228
|
|
|
175
229
|
case 'rename': {
|
|
176
|
-
// Write next to canonical with a -from-<targetfolder>.md suffix
|
|
177
230
|
const base = c.kitPath.replace(/\.md$/, '');
|
|
178
231
|
const tag = path.basename(path.dirname(path.dirname(c.destPath))).replace(/^\./, '');
|
|
179
232
|
const out = `${base}-from-${tag || 'ide'}.md`;
|
|
@@ -189,6 +242,42 @@ async function applyOne(c, strategy, opts) {
|
|
|
189
242
|
}
|
|
190
243
|
}
|
|
191
244
|
|
|
245
|
+
async function applyMirrorTreeOne(c, strategy, dryRun) {
|
|
246
|
+
switch (strategy) {
|
|
247
|
+
case 'skip':
|
|
248
|
+
return 'skipped';
|
|
249
|
+
|
|
250
|
+
case 'overwrite':
|
|
251
|
+
case 'merge': {
|
|
252
|
+
// For framework/hooks files there's no frontmatter to preserve,
|
|
253
|
+
// so 'merge' degenerates to overwrite. Returning a verb that
|
|
254
|
+
// signals the degradation.
|
|
255
|
+
const verb = strategy === 'merge' ? 'merged (overwrite, no frontmatter)' : 'overwritten';
|
|
256
|
+
if (!dryRun) {
|
|
257
|
+
await fs.mkdir(path.dirname(c.kitPath), { recursive: true });
|
|
258
|
+
await fs.copyFile(c.destPath, c.kitPath);
|
|
259
|
+
}
|
|
260
|
+
return dryRun ? `${strategy} (dry-run)` : verb;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case 'rename': {
|
|
264
|
+
// Write to kit/<source>/<rel>.from-<tag> preserving extension after the tag.
|
|
265
|
+
const ext = path.extname(c.kitPath);
|
|
266
|
+
const stem = c.kitPath.slice(0, c.kitPath.length - ext.length);
|
|
267
|
+
const tag = path.basename(path.dirname(path.dirname(c.destPath))).replace(/^\./, '') || 'ide';
|
|
268
|
+
const out = `${stem}.from-${tag}${ext}`;
|
|
269
|
+
if (!dryRun) {
|
|
270
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
271
|
+
await fs.copyFile(c.destPath, out);
|
|
272
|
+
}
|
|
273
|
+
return dryRun ? `rename → ${out} (dry-run)` : `renamed → ${out}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
default:
|
|
277
|
+
return `unknown strategy: ${strategy}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
192
281
|
// --- helpers ---
|
|
193
282
|
|
|
194
283
|
function isCleanStub(content) {
|
package/src/core/sync.js
CHANGED
|
@@ -15,6 +15,8 @@ import { getTarget } from './registry.js';
|
|
|
15
15
|
import { listKit, resolveKitRoot } from './kit.js';
|
|
16
16
|
|
|
17
17
|
const STUB_MARKER = '<!-- kit-mcp:reference -->';
|
|
18
|
+
const MANAGED_MARKER_FILE = '.kit-mcp-managed';
|
|
19
|
+
const MANAGED_MARKER_BODY = '# Managed by @luanpdd/kit-mcp — this directory is overwritten on every `kit sync install`.\n# Do not edit files here directly; edit the canonical source under kit/ and re-run sync.\n# Removing this file disables `kit sync remove` cleanup of this tree.\n';
|
|
18
20
|
|
|
19
21
|
export async function syncTo(targetId, opts = {}) {
|
|
20
22
|
const target = getTarget(targetId);
|
|
@@ -62,21 +64,62 @@ export async function syncTo(targetId, opts = {}) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
// Mirror-tree capabilities (framework, hooks) — copy a whole subtree of kit/<source>
|
|
68
|
+
// into target.<cap>.path, preserving relative structure. Dropped a marker file at the
|
|
69
|
+
// root so `kit sync remove` can clean up the tree safely.
|
|
70
|
+
for (const cap of ['framework', 'hooks']) {
|
|
71
|
+
const spec = target[cap];
|
|
72
|
+
if (!spec || spec.mode !== 'mirror-tree') continue;
|
|
73
|
+
const srcRoot = path.join(kitRoot, spec.source);
|
|
74
|
+
const dstRoot = path.join(projectRoot, spec.path);
|
|
75
|
+
const files = await walkTree(srcRoot);
|
|
76
|
+
if (files.length === 0) continue;
|
|
77
|
+
ops.push({ path: path.join(dstRoot, MANAGED_MARKER_FILE), content: MANAGED_MARKER_BODY, kind: cap });
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
const dst = path.join(dstRoot, f.rel);
|
|
80
|
+
ops.push({ path: dst, srcAbs: f.abs, kind: cap, treeCopy: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
if (!dryRun) {
|
|
66
85
|
for (const op of ops) {
|
|
67
86
|
await fs.mkdir(path.dirname(op.path), { recursive: true });
|
|
68
|
-
|
|
87
|
+
if (op.treeCopy) {
|
|
88
|
+
await fs.copyFile(op.srcAbs, op.path);
|
|
89
|
+
} else {
|
|
90
|
+
await fs.writeFile(op.path, op.content, 'utf8');
|
|
91
|
+
}
|
|
69
92
|
}
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
return { target: targetId, mode, projectRoot, kitRoot, written: ops.map(o => o.path), dryRun };
|
|
73
96
|
}
|
|
74
97
|
|
|
98
|
+
async function walkTree(dir) {
|
|
99
|
+
const out = [];
|
|
100
|
+
async function visit(current, relPrefix) {
|
|
101
|
+
let entries;
|
|
102
|
+
try { entries = await fs.readdir(current, { withFileTypes: true }); }
|
|
103
|
+
catch { return; }
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
const abs = path.join(current, e.name);
|
|
106
|
+
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
107
|
+
if (e.isDirectory()) {
|
|
108
|
+
await visit(abs, rel);
|
|
109
|
+
} else if (e.isFile()) {
|
|
110
|
+
out.push({ abs, rel });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await visit(dir, '');
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
export async function statusOf(targetId, opts = {}) {
|
|
76
119
|
const target = getTarget(targetId);
|
|
77
120
|
const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
|
|
78
121
|
const checks = [];
|
|
79
|
-
for (const cap of ['rules', 'agents', 'commands', 'skills']) {
|
|
122
|
+
for (const cap of ['rules', 'agents', 'commands', 'skills', 'framework', 'hooks']) {
|
|
80
123
|
if (!target[cap]) continue;
|
|
81
124
|
const probe = path.join(projectRoot, target[cap].path);
|
|
82
125
|
let exists = false;
|
|
@@ -105,6 +148,18 @@ export async function removeFrom(targetId, opts = {}) {
|
|
|
105
148
|
}
|
|
106
149
|
} catch {}
|
|
107
150
|
}
|
|
151
|
+
// Mirror-tree capabilities: only remove if our marker is present (we manage the whole subtree).
|
|
152
|
+
for (const cap of ['framework', 'hooks']) {
|
|
153
|
+
const spec = target[cap];
|
|
154
|
+
if (!spec || spec.mode !== 'mirror-tree') continue;
|
|
155
|
+
const dir = path.join(projectRoot, spec.path);
|
|
156
|
+
const marker = path.join(dir, MANAGED_MARKER_FILE);
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(marker);
|
|
159
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
160
|
+
removed.push(dir);
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
108
163
|
return { target: targetId, projectRoot, removed };
|
|
109
164
|
}
|
|
110
165
|
|
|
@@ -141,15 +196,19 @@ function renderReference(item, kitRoot, outPath, isSkill) {
|
|
|
141
196
|
? item.frontmatterRaw
|
|
142
197
|
: synthFrontmatter(item);
|
|
143
198
|
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
199
|
+
// Body must NOT start with the STUB_MARKER comment — IDE listings (e.g. Claude Desktop)
|
|
200
|
+
// that take the first non-blank body line as the visible description would surface
|
|
201
|
+
// "<!-- kit-mcp:reference -->" instead of the real description. So we open with the
|
|
202
|
+
// H1 + description blockquote, and tuck the marker at the end as a trailing comment.
|
|
203
|
+
const descLine = item.description ? `\n> ${item.description}\n` : '';
|
|
204
|
+
return `${fm}
|
|
147
205
|
# ${item.name}
|
|
148
|
-
|
|
206
|
+
${descLine}
|
|
149
207
|
> Canonical source: [\`${rel}\`](${rel})
|
|
150
|
-
${desc}
|
|
151
|
-
> Generated by kit-mcp at ${new Date().toISOString()}.
|
|
152
208
|
> Edit the source file in the kit, not this stub.
|
|
209
|
+
> Generated by kit-mcp at ${new Date().toISOString()}.
|
|
210
|
+
|
|
211
|
+
${STUB_MARKER}
|
|
153
212
|
`;
|
|
154
213
|
}
|
|
155
214
|
|