mini-coder 0.0.19 β 0.0.21
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/.claude/settings.local.json +24 -0
- package/README.md +21 -25
- package/dist/mc-edit.js +251 -0
- package/dist/mc.js +4913 -3753
- package/docs/configs.md +17 -8
- package/docs/custom-agents.md +2 -6
- package/docs/custom-commands.md +7 -6
- package/docs/mini-coder.1.md +20 -22
- package/docs/skills.md +52 -42
- package/package.json +16 -9
- package/research.md +38 -0
- package/docs/tool-hooks.md +0 -142
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebSearch",
|
|
5
|
+
"WebFetch(domain:github.com)",
|
|
6
|
+
"WebFetch(domain:dev.to)",
|
|
7
|
+
"WebFetch(domain:mariozechner.at)",
|
|
8
|
+
"WebFetch(domain:lobste.rs)",
|
|
9
|
+
"WebFetch(domain:reading.sh)",
|
|
10
|
+
"WebFetch(domain:daveswift.com)",
|
|
11
|
+
"Bash(grep:*)",
|
|
12
|
+
"Bash(ls:*)",
|
|
13
|
+
"Bash(bun run:*)",
|
|
14
|
+
"Bash(git stash:*)",
|
|
15
|
+
"Bash(gh pr:*)",
|
|
16
|
+
"Bash(git status:*)",
|
|
17
|
+
"Bash(git add:*)",
|
|
18
|
+
"Bash(git commit:*)",
|
|
19
|
+
"Bash(sqlite3:*)",
|
|
20
|
+
"Bash(git branch:*)",
|
|
21
|
+
"Bash(git checkout:*)"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
package/README.md
CHANGED
|
@@ -26,20 +26,18 @@ I was built with a simple philosophy: **dev flow first**. No slow startup. No cl
|
|
|
26
26
|
|
|
27
27
|
My toolkit is lean on purpose β every tool earns its spot, no passengers:
|
|
28
28
|
|
|
29
|
-
| Tool | What it does
|
|
30
|
-
| --------------- |
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Need more firepower? I connect to **MCP servers** over HTTP or stdio β bolt on external tools whenever the job calls for it.
|
|
29
|
+
| Tool | What it does |
|
|
30
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
31
|
+
| π `shell` | Run shell commands, inspect the repo, and use `mc-edit` for targeted edits |
|
|
32
|
+
| π€ `subagent` | Spawn a focused mini-me for parallel subtasks |
|
|
33
|
+
| π§° `listSkills` | List discovered skills without loading full skill bodies |
|
|
34
|
+
| π `readSkill` | Load one `SKILL.md` on demand |
|
|
35
|
+
| π `webSearch` | Search the web when `EXA_API_KEY` is set |
|
|
36
|
+
| π `webContent` | Fetch full page content from URLs when `EXA_API_KEY` is set |
|
|
37
|
+
|
|
38
|
+
mini-coder is intentionally **shell-first**: inspect with shell, edit with `mc-edit`, verify with shell.
|
|
39
|
+
|
|
40
|
+
Need more firepower? I also connect to **MCP servers** over HTTP or stdio β attached MCP tools show up dynamically whenever the job calls for them.
|
|
43
41
|
|
|
44
42
|
---
|
|
45
43
|
|
|
@@ -49,13 +47,12 @@ Need more firepower? I connect to **MCP servers** over HTTP or stdio β bolt on
|
|
|
49
47
|
- **Built-in web search** β set `EXA_API_KEY` and I expose `webSearch` + `webContent` tools.
|
|
50
48
|
- **Session memory** β conversations are saved in a local SQLite database. Resume where you left off with `-c` or pick a specific session with `-r <id>`.
|
|
51
49
|
- **Shell integration** β prefix with `!` to run shell commands inline. Use `@` to reference files in your prompt (with Tab completion).
|
|
52
|
-
- **Slash commands** β `/model` or `/models` to list/switch models, `/model effort <low|medium|high|xhigh|off>` for reasoning effort, `/reasoning [on|off]` to toggle reasoning display, `/context` to inspect or tune pruning/tool-result caps, `/
|
|
50
|
+
- **Slash commands** β `/model` or `/models` to list/switch models, `/model effort <low|medium|high|xhigh|off>` for reasoning effort, `/reasoning [on|off]` to toggle reasoning display, `/context` to inspect or tune pruning/tool-result caps, `/cache` to configure prompt caching, `/review` for a code review (global custom command, auto-created at `~/.agents/commands/review.md`), `/agent [name]` to set or clear an active primary agent, `/undo` to remove the last conversation turn (it does not revert filesystem changes), `/new` for a clean session, `/mcp list|add|remove` to manage MCP servers, and `/exit` (`/quit`, `/q`) to leave. See all with `/help`.
|
|
53
51
|
|
|
54
52
|
- **Custom commands** β drop a `.md` file in `.agents/commands/` and it becomes a `/command`. Claude-compatible `.claude/commands/` works too. Supports argument placeholders (`$ARGUMENTS`, `$1`β¦`$9`) and shell interpolation (`` !`cmd` ``). Global commands live in `~/.agents/commands/` and `~/.claude/commands/`. Custom commands take precedence over built-ins. β [docs/custom-commands.md](docs/custom-commands.md)
|
|
55
|
-
- **Custom agents** β drop a `.md` file in `.agents/agents/` or `.claude/agents/` (or `~/.agents/agents/` / `~/.claude/agents/` globally) and activate it with `/agent [name]`. Agent definitions are also exposed to subagent delegation unless `mode: primary`.
|
|
53
|
+
- **Custom agents** β drop a `.md` file in `.agents/agents/` or `.claude/agents/` (or `~/.agents/agents/` / `~/.claude/agents/` globally) and activate it with `/agent [name]`. Agent definitions are also exposed to subagent delegation unless `mode: primary`. β [docs/custom-agents.md](docs/custom-agents.md)
|
|
56
54
|
- **Skills** β place a `SKILL.md` in `.agents/skills/<name>/` and inject it into any prompt with `@skill-name`. Claude-compatible `.claude/skills/<name>/SKILL.md` works too. Skills are _never_ auto-loaded β always explicit. β [docs/skills.md](docs/skills.md)
|
|
57
|
-
- **
|
|
58
|
-
- **Beautiful, minimal output** β diffs for edits, formatted trees for file searches, a live status bar with model, git branch, and token counts.
|
|
55
|
+
- **Beautiful, minimal output** β compact tool output, formatted trees for file searches, a live status bar with model, git branch, and token counts.
|
|
59
56
|
- **16 ANSI colors only** β my output inherits _your_ terminal theme. Dark mode, light mode, Solarized, Gruvbox β I fit right in.
|
|
60
57
|
|
|
61
58
|
---
|
|
@@ -64,7 +61,7 @@ Need more firepower? I connect to **MCP servers** over HTTP or stdio β bolt on
|
|
|
64
61
|
|
|
65
62
|
- **I eat my own dog food.** I was built _by_ a mini-coder agent. It's agents all the way down. π’
|
|
66
63
|
- **I'm tiny but mighty.** The whole runtime is [Bun.js](https://bun.com) β fast startup, native TypeScript, and a built-in SQLite driver.
|
|
67
|
-
- **I respect existing conventions.**
|
|
64
|
+
- **I respect existing conventions.** Context lives in `AGENTS.md` or `CLAUDE.md`, commands in `.agents/commands/`, agents in `.agents/agents/`, skills in `.agents/skills/` β I follow the ecosystem instead of inventing new standards.
|
|
68
65
|
- **I spin while I think.** β β β Ήβ Έβ Όβ ΄β ¦β §β β (It's the little things.)
|
|
69
66
|
- **I can clone myself.** The `subagent` tool lets me spin up parallel instances of myself to tackle independent subtasks simultaneously. Divide and conquer! (Up to 10 levels deep.)
|
|
70
67
|
|
|
@@ -82,7 +79,6 @@ I follow the [`.agents` convention](https://github.com/agentsmd/agents) β the
|
|
|
82
79
|
| `.claude/agents/*.md` | Alternate `.claude` path for custom agents |
|
|
83
80
|
| `.agents/skills/<name>/SKILL.md` | Reusable skill instructions (`@name`) |
|
|
84
81
|
| `.claude/skills/<name>/SKILL.md` | Claude-compatible skills |
|
|
85
|
-
| `.agents/hooks/post-<tool>` | Scripts run after supported built-in tool calls |
|
|
86
82
|
| `.agents/AGENTS.md` | Preferred local project context |
|
|
87
83
|
| `CLAUDE.md` | Local fallback context if `.agents/AGENTS.md` is absent |
|
|
88
84
|
| `AGENTS.md` | Local fallback context if `.agents/AGENTS.md` and `CLAUDE.md` are absent |
|
|
@@ -119,7 +115,7 @@ export EXA_API_KEY=your-exa-key # enables webSearch/webContent
|
|
|
119
115
|
mc
|
|
120
116
|
```
|
|
121
117
|
|
|
122
|
-
Or drop me a prompt straight away
|
|
118
|
+
Or drop me a prompt straight away for one-shot mode (runs once, then exits):
|
|
123
119
|
|
|
124
120
|
```bash
|
|
125
121
|
mc "Refactor the auth module to use async/await"
|
|
@@ -142,7 +138,7 @@ mc -h # show help
|
|
|
142
138
|
|
|
143
139
|
Everything I remember lives in `~/.config/mini-coder/` β here's what I'm holding onto:
|
|
144
140
|
|
|
145
|
-
- `sessions.db` β your full session history,
|
|
141
|
+
- `sessions.db` β your full session history, MCP server config, and model metadata, all in one tidy SQLite file
|
|
146
142
|
- `api.log` β a request/response log for every provider call this run, if you want to peek under the hood
|
|
147
143
|
- `errors.log` β anything that went sideways, caught and written down so you can actually debug it
|
|
148
144
|
|
|
@@ -156,7 +152,6 @@ The README hits the highlights β the docs have the full story:
|
|
|
156
152
|
- [docs/custom-agents.md](docs/custom-agents.md)
|
|
157
153
|
- [docs/skills.md](docs/skills.md)
|
|
158
154
|
- [docs/configs.md](docs/configs.md)
|
|
159
|
-
- [docs/tool-hooks.md](docs/tool-hooks.md)
|
|
160
155
|
|
|
161
156
|
## ποΈ Project Structure
|
|
162
157
|
|
|
@@ -166,8 +161,9 @@ src/
|
|
|
166
161
|
agent/ # Main REPL loop + tool registry
|
|
167
162
|
cli/ # Input, output, slash commands, markdown rendering
|
|
168
163
|
llm-api/ # Provider factory + streaming turn logic
|
|
169
|
-
tools/ #
|
|
170
|
-
# + webSearch, webContent
|
|
164
|
+
tools/ # shell, subagent, skill tools
|
|
165
|
+
# + webSearch, webContent
|
|
166
|
+
internal/ # shared internals, including the mc-edit helper implementation
|
|
171
167
|
mcp/ # MCP server connections
|
|
172
168
|
session/ # SQLite-backed session & history management
|
|
173
169
|
```
|
package/dist/mc-edit.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli/structured-output.ts
|
|
5
|
+
import { createTwoFilesPatch } from "diff";
|
|
6
|
+
function normalizePatchLines(patchText) {
|
|
7
|
+
const patchLines = patchText.split(`
|
|
8
|
+
`);
|
|
9
|
+
while (patchLines.at(-1) === "") {
|
|
10
|
+
patchLines.pop();
|
|
11
|
+
}
|
|
12
|
+
const diffLines = patchLines.filter((line) => !line.startsWith("Index: ") && line !== "===================================================================");
|
|
13
|
+
return diffLines.map((line) => {
|
|
14
|
+
if (line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
15
|
+
return line.split("\t", 1)[0] ?? line;
|
|
16
|
+
}
|
|
17
|
+
return line;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function renderUnifiedDiff(filePath, before, after) {
|
|
21
|
+
if (before === after) {
|
|
22
|
+
return "(no changes)";
|
|
23
|
+
}
|
|
24
|
+
const patchText = createTwoFilesPatch(filePath, filePath, before, after, "", "", {
|
|
25
|
+
context: 3
|
|
26
|
+
});
|
|
27
|
+
return normalizePatchLines(patchText).join(`
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function renderMetadataBlock(result) {
|
|
31
|
+
const lines = [`ok: ${result.ok}`];
|
|
32
|
+
if ("path" in result && result.path) {
|
|
33
|
+
lines.push(`path: ${result.path}`);
|
|
34
|
+
}
|
|
35
|
+
if (result.ok) {
|
|
36
|
+
lines.push(`changed: ${result.changed}`);
|
|
37
|
+
} else {
|
|
38
|
+
lines.push(`code: ${result.code}`);
|
|
39
|
+
lines.push(`message: ${result.message}`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join(`
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
function writeFileEditResult(io, result) {
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
const sections = [
|
|
47
|
+
renderUnifiedDiff(result.path, result.before, result.after),
|
|
48
|
+
renderMetadataBlock(result)
|
|
49
|
+
];
|
|
50
|
+
io.stdout(`${sections.join(`
|
|
51
|
+
|
|
52
|
+
`)}
|
|
53
|
+
`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
io.stderr(`${renderMetadataBlock(result)}
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/internal/file-edit/path.ts
|
|
61
|
+
import { homedir } from "os";
|
|
62
|
+
import { join, relative } from "path";
|
|
63
|
+
function stripMatchingQuotes(value) {
|
|
64
|
+
if (value.length < 2)
|
|
65
|
+
return value;
|
|
66
|
+
const first = value[0];
|
|
67
|
+
const last = value[value.length - 1];
|
|
68
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
69
|
+
return value.slice(1, -1);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function normalizePathInput(pathInput) {
|
|
74
|
+
return stripMatchingQuotes(pathInput.trim());
|
|
75
|
+
}
|
|
76
|
+
function resolvePath(cwdInput, pathInput) {
|
|
77
|
+
const cwd = cwdInput ?? process.cwd();
|
|
78
|
+
const normalizedInput = normalizePathInput(pathInput);
|
|
79
|
+
const expanded = normalizedInput.startsWith("~/") ? join(homedir(), normalizedInput.slice(2)) : normalizedInput === "~" ? homedir() : normalizedInput;
|
|
80
|
+
const filePath = expanded.startsWith("/") ? expanded : join(cwd, expanded);
|
|
81
|
+
const relPath = relative(cwd, filePath);
|
|
82
|
+
return { cwd, filePath, relPath };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/internal/file-edit/exact-text.ts
|
|
86
|
+
class FileEditError extends Error {
|
|
87
|
+
code;
|
|
88
|
+
constructor(code, message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.name = "FileEditError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function findExactMatchOffsets(source, target) {
|
|
95
|
+
if (target.length === 0) {
|
|
96
|
+
throw new FileEditError("empty_old_text", "Expected text must be non-empty.");
|
|
97
|
+
}
|
|
98
|
+
const matches = [];
|
|
99
|
+
let searchStart = 0;
|
|
100
|
+
while (searchStart <= source.length - target.length) {
|
|
101
|
+
const matchIndex = source.indexOf(target, searchStart);
|
|
102
|
+
if (matchIndex === -1)
|
|
103
|
+
break;
|
|
104
|
+
matches.push(matchIndex);
|
|
105
|
+
searchStart = matchIndex + 1;
|
|
106
|
+
}
|
|
107
|
+
return matches;
|
|
108
|
+
}
|
|
109
|
+
function planExactTextEdit(source, oldText, newText) {
|
|
110
|
+
const matches = findExactMatchOffsets(source, oldText);
|
|
111
|
+
if (matches.length === 0) {
|
|
112
|
+
throw new FileEditError("target_not_found", "Expected text was not found in the file.");
|
|
113
|
+
}
|
|
114
|
+
if (matches.length > 1) {
|
|
115
|
+
throw new FileEditError("target_not_unique", "Expected text matched multiple locations in the file.");
|
|
116
|
+
}
|
|
117
|
+
const matchIndex = matches[0] ?? 0;
|
|
118
|
+
const updated = source.slice(0, matchIndex) + newText + source.slice(matchIndex + oldText.length);
|
|
119
|
+
return {
|
|
120
|
+
updated,
|
|
121
|
+
changed: updated !== source
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function applyExactTextEdit(input) {
|
|
125
|
+
const { filePath, relPath } = resolvePath(input.cwd, input.path);
|
|
126
|
+
const file = Bun.file(filePath);
|
|
127
|
+
if (!await file.exists()) {
|
|
128
|
+
throw new FileEditError("file_not_found", `File not found: "${relPath}".`);
|
|
129
|
+
}
|
|
130
|
+
const original = await file.text();
|
|
131
|
+
const plan = planExactTextEdit(original, input.oldText, input.newText);
|
|
132
|
+
if (plan.changed) {
|
|
133
|
+
await Bun.write(filePath, plan.updated);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
path: relPath,
|
|
137
|
+
changed: plan.changed,
|
|
138
|
+
before: original,
|
|
139
|
+
after: plan.updated
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/internal/file-edit/cli.ts
|
|
144
|
+
var HELP = `Usage: mc-edit <path> (--old <text> | --old-file <path>) [--new <text> | --new-file <path>] [--cwd <path>]
|
|
145
|
+
|
|
146
|
+
Apply one safe exact-text edit to an existing file.
|
|
147
|
+
- The expected old text must match exactly once.
|
|
148
|
+
- Omit --new / --new-file to delete the matched text.
|
|
149
|
+
- Success output is human-oriented: plain unified diff first, metadata second.`;
|
|
150
|
+
async function readArgText(flag, filePath) {
|
|
151
|
+
const file = Bun.file(filePath);
|
|
152
|
+
if (!await file.exists()) {
|
|
153
|
+
throw new FileEditError("file_not_found", `${flag} file not found: "${filePath}".`);
|
|
154
|
+
}
|
|
155
|
+
return file.text();
|
|
156
|
+
}
|
|
157
|
+
async function parseFileEditCliArgs(argv) {
|
|
158
|
+
let cwd = process.cwd();
|
|
159
|
+
let path = null;
|
|
160
|
+
let oldText = null;
|
|
161
|
+
let oldFilePath = null;
|
|
162
|
+
let newText = null;
|
|
163
|
+
let newFilePath = null;
|
|
164
|
+
for (let i = 0;i < argv.length; i++) {
|
|
165
|
+
const arg = argv[i] ?? "";
|
|
166
|
+
switch (arg) {
|
|
167
|
+
case "--help":
|
|
168
|
+
case "-h":
|
|
169
|
+
return null;
|
|
170
|
+
case "--cwd":
|
|
171
|
+
cwd = argv[++i] ?? process.cwd();
|
|
172
|
+
break;
|
|
173
|
+
case "--old":
|
|
174
|
+
oldText = argv[++i] ?? "";
|
|
175
|
+
break;
|
|
176
|
+
case "--old-file":
|
|
177
|
+
oldFilePath = argv[++i] ?? null;
|
|
178
|
+
break;
|
|
179
|
+
case "--new":
|
|
180
|
+
newText = argv[++i] ?? "";
|
|
181
|
+
break;
|
|
182
|
+
case "--new-file":
|
|
183
|
+
newFilePath = argv[++i] ?? null;
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
if (arg.startsWith("-")) {
|
|
187
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
188
|
+
}
|
|
189
|
+
if (path !== null) {
|
|
190
|
+
throw new Error("Expected exactly one positional <path> argument.");
|
|
191
|
+
}
|
|
192
|
+
path = arg;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (path === null) {
|
|
196
|
+
throw new Error("Missing required <path> argument.");
|
|
197
|
+
}
|
|
198
|
+
if (oldText === null === (oldFilePath === null)) {
|
|
199
|
+
throw new Error("Provide exactly one of --old or --old-file.");
|
|
200
|
+
}
|
|
201
|
+
if (newText !== null && newFilePath !== null) {
|
|
202
|
+
throw new Error("Provide at most one of --new or --new-file.");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
cwd,
|
|
206
|
+
path,
|
|
207
|
+
oldText: oldText ?? await readArgText("--old-file", oldFilePath ?? ""),
|
|
208
|
+
newText: newText ?? (newFilePath ? await readArgText("--new-file", newFilePath) : "")
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function buildCliFailure(code, message, path) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
code,
|
|
215
|
+
message,
|
|
216
|
+
...path ? { path } : {}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function normalizeCliError(error, path) {
|
|
220
|
+
if (error instanceof FileEditError) {
|
|
221
|
+
return buildCliFailure(error.code, error.message, path);
|
|
222
|
+
}
|
|
223
|
+
if (error instanceof Error) {
|
|
224
|
+
return buildCliFailure("invalid_args", error.message, path);
|
|
225
|
+
}
|
|
226
|
+
return buildCliFailure("invalid_args", "Unknown error.", path);
|
|
227
|
+
}
|
|
228
|
+
async function runFileEditCli(argv, io = {
|
|
229
|
+
stdout: (text) => process.stdout.write(text),
|
|
230
|
+
stderr: (text) => process.stderr.write(text)
|
|
231
|
+
}) {
|
|
232
|
+
let parsed = null;
|
|
233
|
+
try {
|
|
234
|
+
parsed = await parseFileEditCliArgs(argv);
|
|
235
|
+
if (parsed === null) {
|
|
236
|
+
io.stderr(`${HELP}
|
|
237
|
+
`);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const result = await applyExactTextEdit(parsed);
|
|
241
|
+
writeFileEditResult(io, { ok: true, ...result });
|
|
242
|
+
return 0;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
writeFileEditResult(io, normalizeCliError(error, parsed?.path));
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/mc-edit.ts
|
|
250
|
+
var exitCode = await runFileEditCli(process.argv.slice(2));
|
|
251
|
+
process.exit(exitCode);
|