mcpill 1.3.0 → 1.6.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.
@@ -1,270 +0,0 @@
1
- ---
2
- title: "Feature Specification"
3
- type: planning
4
- tags: [planning, specification, scoping]
5
- ---
6
-
7
- # Specification: mcpill compile-hooks — Manual Test Guide
8
-
9
- **Date:** 2026-05-26
10
- **Author:** ruco
11
- **Status:** Draft
12
-
13
- ---
14
-
15
- ## Overview
16
-
17
- > `mcpill compile` auto-generates a `PreToolUse` hook in `.claude/settings.json` that warns Claude to use the pill's MCP tools instead of native file tools (`Read`, `Bash`, `Edit`, `Write`). The hook derives its matcher and message from the `## Tools` table in `AGENT.md`, making each pill self-enforcing without manual setup. Key behaviours: merge without overwrite, `--no-hooks` opt-out, and `mcpill validate` warning when the hook is absent.
18
-
19
- ---
20
-
21
- ## Background & Motivation
22
-
23
- Without this feature, a pill author must manually wire a `PreToolUse` hook into `.claude/settings.json` every time they publish or update a pill. Agents forget to use the registered MCP tools and fall back to native tools, bypassing the pill entirely. The compile step already has all the information needed (pill name, tool names, which native tools each replaces) — generating the hook at compile time ties it to the source of truth.
24
-
25
- ## Goals
26
-
27
- - `mcpill compile` writes or updates a `PreToolUse` hook in `<project>/.claude/settings.json` derived from the pill's `AGENT.md ## Tools` table.
28
- - Existing entries in `settings.json` (e.g. `mcpServers`) are preserved; only the matching `PreToolUse` entry is replaced.
29
- - `--no-hooks` opts out and writes `.mcpill/.no-hooks` as a sentinel so `validate` skips the check.
30
- - `mcpill validate` emits a non-fatal warning when `AGENT.md` declares replacements but no hook is present.
31
-
32
- ## Non-Goals
33
-
34
- - Auto-generating `AGENT.md` — the pill author writes it manually.
35
- - Writing to the global `~/.claude/settings.json` — scope is always the project root.
36
- - Enforcing hook presence (fatal error) — the warning is advisory only.
37
-
38
- ## Detailed Design
39
-
40
- ### Data Model
41
-
42
- ```
43
- AGENT.md ## Tools table — expected columns (order-independent, case-insensitive header):
44
- | Tool | Description | Replaces |
45
- |--------------|-------------------|---------------|
46
- | read-chunks | Read file chunks | Read |
47
- | run-query | Execute SQL | Bash,Read |
48
-
49
- "Replaces" cell: comma-separated native tool names (Read, Bash, Edit, Write).
50
- Omitting the column or leaving a cell blank → no hook generated for that row.
51
-
52
- .claude/settings.json — hooks section written by compile:
53
- {
54
- "hooks": {
55
- "PreToolUse": [
56
- {
57
- "matcher": "Read|Bash",
58
- "hooks": [{ "type": "command", "command": "echo \"[mcpill:<name>] Use pill tools instead: <tool>→<native>; ... See .mcpill/AGENT.md.\"" }]
59
- }
60
- ]
61
- }
62
- }
63
-
64
- Merge key: matcher string (exact match). Replace entry if matcher already exists; append otherwise.
65
-
66
- .mcpill/.no-hooks — presence signals that hook generation was opted out; written by --no-hooks flag.
67
- ```
68
-
69
- ### API / Interface
70
-
71
- ```
72
- # CLI
73
- mcpill compile [--dir <path>] [--no-hooks]
74
-
75
- # Programmatic
76
- runCompile(opts: { dir?: string; toMd?: boolean; strict?: boolean; noHooks?: boolean }): Promise<void>
77
-
78
- # AGENT.md lookup order
79
- 1. <baseDir>/AGENT.md
80
- 2. <baseDir>/.mcpill/AGENT.md
81
- (first found wins; if neither exists, hook generation is skipped silently)
82
-
83
- # Hook message format
84
- [mcpill:<pillName>] Use pill tools instead: <tool1>→<native1>,<native2>; <tool2>→<native3>. See .mcpill/AGENT.md.
85
- ```
86
-
87
- ### Behavior
88
-
89
- 1. `mcpill compile` runs the standard compile pipeline (server.md → .mcpill/server/).
90
- 2. If `--no-hooks` was passed: write `.mcpill/.no-hooks`, return early — no hook written.
91
- 3. Locate `AGENT.md` at `baseDir/AGENT.md` or `.mcpill/AGENT.md`; if absent, skip hook generation.
92
- 4. Parse `## Tools` table — find the `replaces` column (case-insensitive). Extract comma-separated native tool names per row. Skip rows with empty/missing replaces cell.
93
- 5. If no rows have replacements, skip hook generation.
94
- 6. Build `HookEntry`: matcher = all unique native tool names joined by `|`; command = static `echo` with pill name and full tool→native summary baked in.
95
- 7. Read `.claude/settings.json` (or start from `{}`). Find existing `hooks.PreToolUse` array entry with same matcher; replace it, or append if not found. Write back.
96
- 8. `mcpill validate` — after structural validation passes, check: if `.mcpill/.no-hooks` absent AND `AGENT.md` declares replacements AND `.claude/settings.json` has no `PreToolUse` entries → emit `⚠ No PreToolUse hook in .claude/settings.json — run mcpill compile to add it`.
97
-
98
- ## Error Handling
99
-
100
- | Error Case | Behavior | User-Facing Message |
101
- |---|---|---|
102
- | `.claude/settings.json` exists but is invalid JSON | `JSON.parse` throws → compile crashes with a JS stack trace | *(raw error)* — fix or delete the malformed settings.json |
103
- | `AGENT.md` exists but has no `## Tools` section | `parseAgentMdTools` returns `[]` → hook generation silently skipped | *(none)* |
104
- | `AGENT.md` has `## Tools` but no `replaces` column | `parseAgentMdTools` returns `[]` → silently skipped | *(none)* |
105
- | Compile run with `--no-hooks`; later `validate` called | `.no-hooks` sentinel present → validate skips hook check entirely | *(none)* |
106
- | `baseDir/.claude/` does not exist | `mkdirSync(claudeDir, { recursive: true })` creates it | *(none — transparent)* |
107
-
108
- ## Security Considerations
109
-
110
- - Hook command is a static `echo` — no shell variable expansion, no user-controlled input interpolated into the command string at runtime. `JSON.stringify(message)` in `buildHookEntry` ensures the baked-in string is safely quoted.
111
- - `settings.json` is scoped to `baseDir/.claude/` — no writes to global config.
112
- - `AGENT.md` content is read and parsed; no `eval`/`exec` of its contents.
113
-
114
- ## Testing Plan
115
-
116
- Run all automated tests first: `npm test` — all 6 hook tests in `src/__tests__/compile.test.ts` must pass.
117
-
118
- Then follow these manual steps against a real pill directory.
119
-
120
- ### Setup
121
-
122
- Create a scratch pill for testing:
123
-
124
- ```sh
125
- mkdir /tmp/test-pill && cd /tmp/test-pill
126
- mcpill init
127
- ```
128
-
129
- Add a minimal `AGENT.md` at the project root:
130
-
131
- ```markdown
132
- # AGENT
133
-
134
- ## Tools
135
-
136
- | Tool | Description | Replaces |
137
- |------|-------------|----------|
138
- | read-chunks | Read a file in chunks | Read |
139
- | run-query | Execute a SQL query | Bash,Read |
140
- ```
141
-
142
- ---
143
-
144
- ### Test 1 — Happy path: hook is written
145
-
146
- - [ ] Run `mcpill compile` in `/tmp/test-pill`.
147
- - [ ] Verify `.claude/settings.json` was created.
148
- - [ ] Open it and confirm:
149
- - `hooks.PreToolUse` is an array with one entry.
150
- - `matcher` is `"Read|Bash"` (or `"Bash|Read"` — order may vary).
151
- - `hooks[0].command` contains `echo`.
152
- - The echo string contains `[mcpill:` followed by the pill name (from `.mcpill/server.md` or `PILL.md`).
153
- - The echo string contains `read-chunks→Read` and `run-query→Bash,Read`.
154
- - The echo string ends with `See .mcpill/AGENT.md.`
155
-
156
- ---
157
-
158
- ### Test 2 — Merge: existing settings.json is preserved
159
-
160
- - [ ] Add a fake `mcpServers` entry to `.claude/settings.json`:
161
- ```json
162
- { "mcpServers": { "some-server": { "command": "node", "args": [] } } }
163
- ```
164
- - [ ] Run `mcpill compile` again.
165
- - [ ] Verify `.claude/settings.json` still contains `mcpServers.some-server`.
166
- - [ ] Verify `hooks.PreToolUse` is also present (not the only key).
167
-
168
- ---
169
-
170
- ### Test 3 — Re-compile replaces the hook, not appends
171
-
172
- - [ ] Change the `Replaces` cell of `run-query` to just `Bash` (remove `Read`).
173
- - [ ] Run `mcpill compile`.
174
- - [ ] Verify `hooks.PreToolUse` still has exactly **one** entry (not two).
175
- - [ ] Verify the matcher reflects the updated set.
176
-
177
- ---
178
-
179
- ### Test 4 — `--no-hooks` flag
180
-
181
- - [ ] Delete `.claude/settings.json` and `.mcpill/.no-hooks` if they exist.
182
- - [ ] Run `mcpill compile --no-hooks`.
183
- - [ ] Verify `.claude/settings.json` was **not** created.
184
- - [ ] Verify `.mcpill/.no-hooks` **was** created (empty file).
185
-
186
- ---
187
-
188
- ### Test 5 — `mcpill validate` warns when hook is absent
189
-
190
- - [ ] Delete `.claude/settings.json` and `.mcpill/.no-hooks`.
191
- - [ ] Run `mcpill compile --no-hooks` (ensures `.no-hooks` is present, then delete it manually).
192
- - [ ] Delete `.mcpill/.no-hooks`.
193
- - [ ] Ensure `AGENT.md` still has a `Replaces` column with values.
194
- - [ ] Run `mcpill validate`.
195
- - [ ] Verify output contains `⚠ No PreToolUse hook in .claude/settings.json`.
196
- - [ ] Verify the exit code is **0** (non-fatal — validate still passes).
197
-
198
- ---
199
-
200
- ### Test 6 — `mcpill validate` skips hook check when `.no-hooks` present
201
-
202
- - [ ] Run `mcpill compile --no-hooks`.
203
- - [ ] Run `mcpill validate`.
204
- - [ ] Verify **no** `⚠ No PreToolUse hook` warning appears in output.
205
-
206
- ---
207
-
208
- ### Test 7 — No AGENT.md: hook silently skipped
209
-
210
- - [ ] Move `AGENT.md` out of the project root (e.g. `/tmp/AGENT.md.bak`).
211
- - [ ] Delete `.claude/settings.json` if present.
212
- - [ ] Run `mcpill compile`.
213
- - [ ] Verify `.claude/settings.json` was **not** created.
214
- - [ ] Restore `AGENT.md`.
215
-
216
- ---
217
-
218
- ### Test 8 — AGENT.md in `.mcpill/` subdirectory
219
-
220
- - [ ] Move `AGENT.md` to `.mcpill/AGENT.md`.
221
- - [ ] Delete `.claude/settings.json` if present.
222
- - [ ] Run `mcpill compile`.
223
- - [ ] Verify `.claude/settings.json` **was** created with the hook.
224
- - [ ] Move `AGENT.md` back to project root.
225
-
226
- ---
227
-
228
- ### Test 9 — AGENT.md with no `replaces` column: no hook
229
-
230
- - [ ] Replace `AGENT.md` content with a table that has no `Replaces` column:
231
- ```markdown
232
- ## Tools
233
- | Tool | Description |
234
- |------|-------------|
235
- | read-chunks | Read a file |
236
- ```
237
- - [ ] Delete `.claude/settings.json`.
238
- - [ ] Run `mcpill compile`.
239
- - [ ] Verify `.claude/settings.json` was **not** created.
240
- - [ ] Restore the original `AGENT.md`.
241
-
242
- ---
243
-
244
- ### Test 10 — Real pill smoke test (xtage or mcpster)
245
-
246
- - [ ] Run `mcpill compile` in a pill repo that already has a real `AGENT.md` with tool replacements.
247
- - [ ] Open `.claude/settings.json` and read the generated hook command.
248
- - [ ] Start a Claude Code session in that repo and deliberately use `Read` on a file.
249
- - [ ] Verify the hook fires and the reminder message is shown in the terminal output.
250
- - [ ] Confirm the message names specific MCP tools and the pill name — not a generic warning.
251
-
252
- ---
253
-
254
- ## Open Questions
255
-
256
- - [ ] Should the hook message include a direct invocation hint (`call read-chunks with path=...`) or keep it as a passive reminder? Currently a passive reminder. Defer until Test 10 reveals whether agents change behavior.
257
- - [ ] Should `mcpill validate` check that the *specific matcher* in `settings.json` covers *all* declared replacements, or just that any `PreToolUse` entry exists? Currently the latter (any entry = ok). Tighten in a follow-up if agents still miss tools.
258
-
259
- ## Alternatives Considered
260
-
261
- | Alternative | Pros | Cons | Decision |
262
- |---|---|---|---|
263
- | Read AGENT.md at hook-trigger time (not baked in) | Always up-to-date without recompile | I/O on every `Read`/`Bash` call; hook latency spikes | Rejected — bake at compile time |
264
- | Write to `~/.claude/settings.json` (global) | Works without project setup | Bleeds hook into every project on the machine | Rejected — project-scoped only |
265
- | Fatal error in `validate` if hook missing | Strong enforcement | Breaks projects that manage hooks separately | Rejected — non-fatal warning only |
266
- | Generate `AGENT.md` at compile time | Less manual work for pill authors | Inverts ownership — `replaces` is semantic knowledge only the author can declare | Rejected — user writes it manually |
267
-
268
- ---
269
-
270
- *Made with [mdblu](https://github.com/ruco-ai/mdblu) · source: `templates/SPEC.md.template`*
@@ -1,203 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import { runCompile } from '../commands/compile.js';
6
-
7
- function scaffoldPill(baseDir: string, name = 'test-pill') {
8
- const mcpillDir = path.join(baseDir, '.mcpill');
9
- fs.mkdirSync(mcpillDir, { recursive: true });
10
- fs.writeFileSync(
11
- path.join(mcpillDir, 'server.md'),
12
- `## Config\nname: ${name}\ntransport: stdio\n`,
13
- );
14
- }
15
-
16
- function makeAgentMd(tools: Array<{ name: string; replaces: string }>): string {
17
- const rows = tools.map((t) => `| ${t.name} | A tool | ${t.replaces} |`).join('\n');
18
- return `# AGENT\n\n## Tools\n\n| Tool | Description | Replaces |\n|------|-------------|----------|\n${rows}\n`;
19
- }
20
-
21
- describe('runCompile — hook generation', () => {
22
- const dirs: string[] = [];
23
-
24
- function mkTmp(): string {
25
- const d = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpill-compile-'));
26
- dirs.push(d);
27
- return d;
28
- }
29
-
30
- afterEach(() => {
31
- vi.restoreAllMocks();
32
- for (const d of dirs.splice(0)) {
33
- fs.rmSync(d, { recursive: true, force: true });
34
- }
35
- });
36
-
37
- it('writes PreToolUse hook when AGENT.md has a replaces column', async () => {
38
- const base = mkTmp();
39
- scaffoldPill(base, 'my-pill');
40
- fs.writeFileSync(
41
- path.join(base, 'AGENT.md'),
42
- makeAgentMd([{ name: 'read-chunks', replaces: 'Read' }]),
43
- );
44
- vi.spyOn(console, 'log').mockImplementation(() => {});
45
-
46
- await runCompile({ dir: base });
47
-
48
- const settingsPath = path.join(base, '.claude', 'settings.json');
49
- expect(fs.existsSync(settingsPath)).toBe(true);
50
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
51
- expect(settings.hooks?.PreToolUse).toBeDefined();
52
- expect(settings.hooks.PreToolUse[0].matcher).toBe('Read');
53
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('read-chunks');
54
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('my-pill');
55
- });
56
-
57
- it('merges hook without overwriting existing settings.json entries', async () => {
58
- const base = mkTmp();
59
- scaffoldPill(base, 'my-pill');
60
- fs.writeFileSync(
61
- path.join(base, 'AGENT.md'),
62
- makeAgentMd([{ name: 'read-chunks', replaces: 'Read' }]),
63
- );
64
-
65
- const claudeDir = path.join(base, '.claude');
66
- fs.mkdirSync(claudeDir, { recursive: true });
67
- fs.writeFileSync(
68
- path.join(claudeDir, 'settings.json'),
69
- JSON.stringify({ mcpServers: { 'existing-server': { command: 'node', args: [] } } }, null, 2),
70
- );
71
- vi.spyOn(console, 'log').mockImplementation(() => {});
72
-
73
- await runCompile({ dir: base });
74
-
75
- const settings = JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8'));
76
- expect(settings.mcpServers?.['existing-server']).toBeDefined();
77
- expect(settings.hooks?.PreToolUse).toBeDefined();
78
- });
79
-
80
- it('builds matcher from multiple tools with overlapping replaces', async () => {
81
- const base = mkTmp();
82
- scaffoldPill(base, 'my-pill');
83
- fs.writeFileSync(
84
- path.join(base, 'AGENT.md'),
85
- makeAgentMd([
86
- { name: 'read-chunks', replaces: 'Read' },
87
- { name: 'run-query', replaces: 'Bash,Read' },
88
- ]),
89
- );
90
- vi.spyOn(console, 'log').mockImplementation(() => {});
91
-
92
- await runCompile({ dir: base });
93
-
94
- const settings = JSON.parse(
95
- fs.readFileSync(path.join(base, '.claude', 'settings.json'), 'utf-8'),
96
- );
97
- const matcher: string = settings.hooks.PreToolUse[0].matcher;
98
- expect(matcher.split('|').sort()).toEqual(['Bash', 'Read'].sort());
99
- });
100
-
101
- it('skips hook when AGENT.md has no replaces column', async () => {
102
- const base = mkTmp();
103
- scaffoldPill(base, 'my-pill');
104
- fs.writeFileSync(
105
- path.join(base, 'AGENT.md'),
106
- '# AGENT\n\n## Tools\n\n| Tool | Description |\n|------|-------------|\n| read-chunks | A tool |\n',
107
- );
108
- vi.spyOn(console, 'log').mockImplementation(() => {});
109
-
110
- await runCompile({ dir: base });
111
-
112
- expect(fs.existsSync(path.join(base, '.claude', 'settings.json'))).toBe(false);
113
- });
114
-
115
- it('skips hook and writes sentinel with --no-hooks', async () => {
116
- const base = mkTmp();
117
- scaffoldPill(base, 'my-pill');
118
- fs.writeFileSync(
119
- path.join(base, 'AGENT.md'),
120
- makeAgentMd([{ name: 'read-chunks', replaces: 'Read' }]),
121
- );
122
- vi.spyOn(console, 'log').mockImplementation(() => {});
123
-
124
- await runCompile({ dir: base, noHooks: true });
125
-
126
- expect(fs.existsSync(path.join(base, '.claude', 'settings.json'))).toBe(false);
127
- expect(fs.existsSync(path.join(base, '.mcpill', '.no-hooks'))).toBe(true);
128
- });
129
-
130
- it('skips hook when no AGENT.md is present', async () => {
131
- const base = mkTmp();
132
- scaffoldPill(base, 'my-pill');
133
- vi.spyOn(console, 'log').mockImplementation(() => {});
134
-
135
- await runCompile({ dir: base });
136
-
137
- expect(fs.existsSync(path.join(base, '.claude', 'settings.json'))).toBe(false);
138
- });
139
-
140
- it('writes PreToolUse hook from PILL.md replaces field', async () => {
141
- const base = mkTmp();
142
- scaffoldPill(base, 'my-pill');
143
- fs.writeFileSync(
144
- path.join(base, 'PILL.md'),
145
- `# Pill: my-pill\n\n---\n\n## Tool: read-chunks\n\ndescription: Read by chunks.\nreplaces: Read\nbehavior: |\n impl\n`,
146
- );
147
- vi.spyOn(console, 'log').mockImplementation(() => {});
148
-
149
- await runCompile({ dir: base });
150
-
151
- const settings = JSON.parse(
152
- fs.readFileSync(path.join(base, '.claude', 'settings.json'), 'utf-8'),
153
- );
154
- expect(settings.hooks?.PreToolUse[0].matcher).toBe('Read');
155
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('read-chunks');
156
- });
157
-
158
- it('merges PILL.md and AGENT.md tools, AGENT.md wins on name conflict', async () => {
159
- const base = mkTmp();
160
- scaffoldPill(base, 'my-pill');
161
- fs.writeFileSync(
162
- path.join(base, 'PILL.md'),
163
- `# Pill: my-pill\n\n---\n\n## Tool: read-chunks\n\ndescription: Read.\nreplaces: Read\nbehavior: |\n impl\n`,
164
- );
165
- // AGENT.md overrides read-chunks to replace Bash instead
166
- fs.writeFileSync(
167
- path.join(base, 'AGENT.md'),
168
- makeAgentMd([{ name: 'read-chunks', replaces: 'Bash' }]),
169
- );
170
- vi.spyOn(console, 'log').mockImplementation(() => {});
171
-
172
- await runCompile({ dir: base });
173
-
174
- const settings = JSON.parse(
175
- fs.readFileSync(path.join(base, '.claude', 'settings.json'), 'utf-8'),
176
- );
177
- const matcher: string = settings.hooks.PreToolUse[0].matcher;
178
- // AGENT.md wins: Bash not Read
179
- expect(matcher).toBe('Bash');
180
- });
181
-
182
- it('combines tools from PILL.md and AGENT.md when names differ', async () => {
183
- const base = mkTmp();
184
- scaffoldPill(base, 'my-pill');
185
- fs.writeFileSync(
186
- path.join(base, 'PILL.md'),
187
- `# Pill: my-pill\n\n---\n\n## Tool: read-chunks\n\ndescription: Read.\nreplaces: Read\nbehavior: |\n impl\n`,
188
- );
189
- fs.writeFileSync(
190
- path.join(base, 'AGENT.md'),
191
- makeAgentMd([{ name: 'run-query', replaces: 'Bash' }]),
192
- );
193
- vi.spyOn(console, 'log').mockImplementation(() => {});
194
-
195
- await runCompile({ dir: base });
196
-
197
- const settings = JSON.parse(
198
- fs.readFileSync(path.join(base, '.claude', 'settings.json'), 'utf-8'),
199
- );
200
- const matcher: string = settings.hooks.PreToolUse[0].matcher;
201
- expect(matcher.split('|').sort()).toEqual(['Bash', 'Read'].sort());
202
- });
203
- });
@@ -1,75 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
2
- import fs from "fs";
3
- import path from "path";
4
- import os from "os";
5
- import { runInit } from "../commands/init.js";
6
-
7
- describe("runInit", () => {
8
- const dirs: string[] = [];
9
-
10
- function mkTmp(): string {
11
- const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-init-"));
12
- dirs.push(d);
13
- return d;
14
- }
15
-
16
- afterEach(() => {
17
- vi.restoreAllMocks();
18
- for (const d of dirs.splice(0)) {
19
- fs.rmSync(d, { recursive: true, force: true });
20
- }
21
- });
22
-
23
- it("scaffolds the expected files and directories", async () => {
24
- const base = mkTmp();
25
- vi.spyOn(console, "log").mockImplementation(() => {});
26
-
27
- await runInit({ dir: base });
28
-
29
- const mcpillDir = path.join(base, ".mcpill");
30
- const serverDir = path.join(mcpillDir, "server");
31
-
32
- expect(fs.existsSync(mcpillDir)).toBe(true);
33
- expect(fs.existsSync(path.join(mcpillDir, "server.md"))).toBe(true);
34
- expect(fs.existsSync(path.join(mcpillDir, "pill-agent-guide.md"))).toBe(true);
35
- expect(fs.existsSync(path.join(mcpillDir, "pill-user-guide.md"))).toBe(true);
36
- expect(fs.existsSync(path.join(serverDir, "mcpill.config.json"))).toBe(true);
37
- expect(fs.existsSync(path.join(serverDir, "tools", "echo.md"))).toBe(true);
38
- expect(fs.existsSync(path.join(serverDir, "prompts", "greeting.md"))).toBe(true);
39
- expect(fs.existsSync(path.join(base, "PILL.md"))).toBe(true);
40
- expect(fs.existsSync(path.join(base, "README.md"))).toBe(false);
41
- expect(fs.existsSync(path.join(base, "package.json"))).toBe(false);
42
-
43
- const config = JSON.parse(
44
- fs.readFileSync(path.join(serverDir, "mcpill.config.json"), "utf-8")
45
- );
46
- expect(config).toMatchObject({ name: path.basename(base), transport: "stdio", port: 3333 });
47
-
48
- const serverMd = fs.readFileSync(path.join(mcpillDir, "server.md"), "utf-8");
49
- expect(serverMd).toContain(`name: ${path.basename(base)}`);
50
-
51
- const pillMd = fs.readFileSync(path.join(base, "PILL.md"), "utf-8");
52
- expect(pillMd).toContain(".mcpill/pill-agent-guide.md");
53
- });
54
-
55
- it("exits with code 1 and correct message when .mcpill/ already exists", async () => {
56
- const base = mkTmp();
57
- vi.spyOn(console, "log").mockImplementation(() => {});
58
- vi.spyOn(console, "error").mockImplementation(() => {});
59
-
60
- await runInit({ dir: base });
61
-
62
- const exitSpy = vi
63
- .spyOn(process, "exit")
64
- .mockImplementation((_code?: number | string | null) => {
65
- throw new Error("process.exit called");
66
- });
67
- const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
68
-
69
- await expect(runInit({ dir: base })).rejects.toThrow("process.exit called");
70
- expect(exitSpy).toHaveBeenCalledWith(1);
71
- expect(errSpy).toHaveBeenCalledWith(
72
- ".mcpill/ already exists. Remove it manually to re-init."
73
- );
74
- });
75
- });
@@ -1,54 +0,0 @@
1
- import { describe, it, expect, afterEach } from "vitest";
2
- import fs from "fs";
3
- import path from "path";
4
- import os from "os";
5
- import { loadConfig } from "../../loaders/config.js";
6
-
7
- describe("loadConfig", () => {
8
- const dirs: string[] = [];
9
-
10
- function mkTmp(): string {
11
- const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-config-"));
12
- dirs.push(d);
13
- return d;
14
- }
15
-
16
- afterEach(() => {
17
- for (const d of dirs.splice(0)) {
18
- fs.rmSync(d, { recursive: true, force: true });
19
- }
20
- });
21
-
22
- it("returns defaults when no mcpill.config.json exists", async () => {
23
- const dir = mkTmp();
24
- const config = await loadConfig(dir);
25
- expect(config).toEqual({ name: "mcpill-server", transport: "stdio", port: 3333 });
26
- });
27
-
28
- it("file values override defaults", async () => {
29
- const dir = mkTmp();
30
- fs.writeFileSync(
31
- path.join(dir, "mcpill.config.json"),
32
- JSON.stringify({ name: "my-server", port: 4000 })
33
- );
34
- const config = await loadConfig(dir);
35
- expect(config).toEqual({ name: "my-server", transport: "stdio", port: 4000 });
36
- });
37
-
38
- it("CLI opts override file values", async () => {
39
- const dir = mkTmp();
40
- fs.writeFileSync(
41
- path.join(dir, "mcpill.config.json"),
42
- JSON.stringify({ transport: "http", port: 4444 })
43
- );
44
- const config = await loadConfig(dir);
45
-
46
- // Simulate CLI opt override (same merge logic used in runServer)
47
- const cliOpts = { transport: "stdio" as const, port: 9999 };
48
- const finalTransport = cliOpts.transport ?? config.transport;
49
- const finalPort = cliOpts.port ?? config.port;
50
-
51
- expect(finalTransport).toBe("stdio");
52
- expect(finalPort).toBe(9999);
53
- });
54
- });