@akms/mcp 0.0.0-stage → 0.1.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 +13 -0
- package/LICENSE.md +11 -0
- package/README.md +61 -2
- package/lib/cli.js +767 -0
- package/package.json +31 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@akms/mcp`.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 — Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- One terminal menu for database, SSH and WSL connection setup, using the selection and rendering behavior of the existing MCP packages.
|
|
10
|
+
- Client selection for Claude Code, Codex, Claude Desktop and Cursor, plus direct commands such as `akms-mcp db --target codex`.
|
|
11
|
+
- A fresh npm `latest` lookup before every launch. The selected version runs through npm without making the MCP packages runtime dependencies of the launcher.
|
|
12
|
+
- Every child opens through its `config` command. Exiting the child returns to the launcher menu.
|
|
13
|
+
- `.npmignore` controls the published package contents, excluding source, tests and development files.
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## MIT License
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright © 2026 Alkemic Studio.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
8
|
+
|
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @akms/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One terminal menu for configuring AKMS database, SSH and WSL MCP connections in your AI client.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx -y --prefer-online @akms/mcp@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Requires **Node.js 22+ and npm**. WSL setup runs from Windows.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
? AKMS MCP · Connection setup
|
|
13
|
+
❯ Database PostgreSQL connections
|
|
14
|
+
SSH Remote hosts
|
|
15
|
+
WSL Local Linux distros · Windows
|
|
16
|
+
────────────────────────
|
|
17
|
+
Client Automatically selected by each MCP
|
|
18
|
+
Quit
|
|
19
|
+
|
|
20
|
+
↑↓ move · enter select · esc back
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Choose a server type to open its current setup screen. Exiting that screen returns to this menu. Use **Client** to select Claude Code, Codex, Claude Desktop or Cursor for subsequent launches. With Automatic, each MCP detects the client using its own setup behavior.
|
|
24
|
+
|
|
25
|
+
## Install and run
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g @akms/mcp
|
|
29
|
+
akms-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`akms-mcp config` and `akms-mcp setup` open the same menu. You can also jump straight into a setup screen:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
akms-mcp db --target codex
|
|
36
|
+
akms-mcp ssh --target claude-code
|
|
37
|
+
akms-mcp wsl --target cursor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `--help` for the command reference and `--version` for the launcher's version.
|
|
41
|
+
|
|
42
|
+
## Latest on every launch
|
|
43
|
+
|
|
44
|
+
Each time you choose a server, the launcher queries npm for that package's `latest` tag, then runs the exact version returned. This applies even when you select the same server twice in one session. If the lookup fails, setup does not start with a cached older version. An internet connection to your configured npm registry is required.
|
|
45
|
+
|
|
46
|
+
The MCP packages are **not dependencies** of this package. npm downloads the selected release into its execution cache when needed; it does not install it into this package or globally. Updating an MCP does not require a new launcher release while its setup command remains compatible.
|
|
47
|
+
|
|
48
|
+
| Selection | Package | Setup command |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Database | `@akms/mcp-db` | `config` |
|
|
51
|
+
| SSH | `@akms/mcp-ssh` | `config` |
|
|
52
|
+
| WSL | `@akms/mcp-wsl` | `config` |
|
|
53
|
+
|
|
54
|
+
The selected MCP owns connection editing, validation and client configuration writes. Entries saved for your AI client follow that MCP's own behavior; this launcher checks versions when opening setup.
|
|
55
|
+
|
|
56
|
+
## Terminal controls
|
|
57
|
+
|
|
58
|
+
Use arrow keys and Enter, or number keys to move to a row. Esc returns to the previous menu; Esc at the main menu exits. The menu uses the same selection and rendering code as the SSH and WSL packages.
|
|
59
|
+
|
|
60
|
+
Outside an interactive terminal, menus show numbers and accept a number, key or label. For scripted setup, the direct command is easiest: `akms-mcp db --target codex` passes stdin to the database setup screen.
|
|
61
|
+
|
|
62
|
+
See [development notes](./README_DEV.md) for build and test instructions.
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/_defs/index.ts
|
|
4
|
+
var MCP_PACKAGES = {
|
|
5
|
+
db: { label: "Database", packageName: "@akms/mcp-db", command: "akms-mcp-db", setupCommand: "config" },
|
|
6
|
+
ssh: { label: "SSH", packageName: "@akms/mcp-ssh", command: "akms-mcp-ssh", setupCommand: "config" },
|
|
7
|
+
wsl: { label: "WSL", packageName: "@akms/mcp-wsl", command: "akms-mcp-wsl", setupCommand: "config" }
|
|
8
|
+
};
|
|
9
|
+
var CLIENT_LABELS = {
|
|
10
|
+
"claude-code": "Claude Code",
|
|
11
|
+
codex: "Codex",
|
|
12
|
+
"claude-desktop": "Claude Desktop",
|
|
13
|
+
cursor: "Cursor"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/modules/launcher/arguments.ts
|
|
17
|
+
function parseArguments(argv) {
|
|
18
|
+
const parsed = { showHelp: false, showVersion: false };
|
|
19
|
+
let commandSeen = false;
|
|
20
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
21
|
+
const argument = argv[index];
|
|
22
|
+
if (argument === "--help" || argument === "-h") {
|
|
23
|
+
parsed.showHelp = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (argument === "--version" || argument === "-v") {
|
|
27
|
+
parsed.showVersion = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (argument === "--target" || argument.startsWith("--target=") == true) {
|
|
31
|
+
let target = argument.slice("--target=".length);
|
|
32
|
+
if (argument === "--target") {
|
|
33
|
+
index += 1;
|
|
34
|
+
target = argv[index];
|
|
35
|
+
}
|
|
36
|
+
if (target == null || Object.hasOwn(CLIENT_LABELS, target) == false) {
|
|
37
|
+
throw new Error("--target needs one of: claude-code, codex, claude-desktop, cursor");
|
|
38
|
+
}
|
|
39
|
+
parsed.targetId = target;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if ((argument === "setup" || argument === "config") && commandSeen == false && parsed.mcpId == null) {
|
|
43
|
+
commandSeen = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(MCP_PACKAGES, argument) == true && parsed.mcpId == null) {
|
|
47
|
+
parsed.mcpId = argument;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unknown argument: ${argument}. Run akms-mcp --help for usage.`);
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/modules/launcher/prompter.ts
|
|
56
|
+
var PromptClosedError = class extends Error {
|
|
57
|
+
constructor() {
|
|
58
|
+
super("input closed before the prompt was answered");
|
|
59
|
+
this.name = "PromptClosedError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var PromptBackError = class extends Error {
|
|
63
|
+
constructor() {
|
|
64
|
+
super("prompt left with esc");
|
|
65
|
+
this.name = "PromptBackError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var ESC = "\x1B";
|
|
69
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
70
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
71
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
72
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
73
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
74
|
+
var KEY_CTRL_C = "";
|
|
75
|
+
var KEY_CTRL_D = "";
|
|
76
|
+
function splitKeys(chunk) {
|
|
77
|
+
const keys = [];
|
|
78
|
+
let index = 0;
|
|
79
|
+
while (index < chunk.length) {
|
|
80
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
81
|
+
if (isSequence == true) {
|
|
82
|
+
let end = index + 2;
|
|
83
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
84
|
+
end += 1;
|
|
85
|
+
}
|
|
86
|
+
keys.push(chunk.slice(index, end + 1));
|
|
87
|
+
index = end + 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
91
|
+
const character = String.fromCodePoint(codePoint);
|
|
92
|
+
keys.push(character);
|
|
93
|
+
index += character.length;
|
|
94
|
+
}
|
|
95
|
+
return keys;
|
|
96
|
+
}
|
|
97
|
+
function charWidth(codePoint) {
|
|
98
|
+
if (codePoint < 4352) {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
102
|
+
if (isWide == true) {
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
function displayWidth(text) {
|
|
108
|
+
let width = 0;
|
|
109
|
+
for (const character of text) {
|
|
110
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
111
|
+
}
|
|
112
|
+
return width;
|
|
113
|
+
}
|
|
114
|
+
function padToWidth(text, width) {
|
|
115
|
+
const missing = width - displayWidth(text);
|
|
116
|
+
if (missing <= 0) {
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
return `${text}${" ".repeat(missing)}`;
|
|
120
|
+
}
|
|
121
|
+
function groupLabelWidths(items) {
|
|
122
|
+
const widths = items.map(() => 0);
|
|
123
|
+
let groupStart = 0;
|
|
124
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
125
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
126
|
+
if (atBoundary == false) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let groupWidth = 0;
|
|
130
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
131
|
+
const item = items[member];
|
|
132
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
133
|
+
}
|
|
134
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
135
|
+
widths[member] = groupWidth;
|
|
136
|
+
}
|
|
137
|
+
groupStart = index + 1;
|
|
138
|
+
}
|
|
139
|
+
return widths;
|
|
140
|
+
}
|
|
141
|
+
function supportsUnicode() {
|
|
142
|
+
if (process.platform !== "win32") {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
146
|
+
}
|
|
147
|
+
function createStyle(enabled) {
|
|
148
|
+
const paint = (open, close) => {
|
|
149
|
+
return (text) => {
|
|
150
|
+
if (enabled == false || text === "") {
|
|
151
|
+
return text;
|
|
152
|
+
}
|
|
153
|
+
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
accent: paint(36, 39),
|
|
158
|
+
success: paint(32, 39),
|
|
159
|
+
dim: paint(2, 22),
|
|
160
|
+
bold: paint(1, 22)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
var Prompter = class {
|
|
164
|
+
constructor(input, output) {
|
|
165
|
+
this.input = input;
|
|
166
|
+
this.output = output;
|
|
167
|
+
const ttyInput = input;
|
|
168
|
+
const ttyOutput = output;
|
|
169
|
+
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
170
|
+
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
171
|
+
if (supportsUnicode() == true) {
|
|
172
|
+
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
173
|
+
} else {
|
|
174
|
+
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
175
|
+
}
|
|
176
|
+
this.inputEnded = input.readableEnded;
|
|
177
|
+
input.setEncoding("utf8");
|
|
178
|
+
input.on("data", this.onInputData);
|
|
179
|
+
input.on("end", this.onInputEnd);
|
|
180
|
+
}
|
|
181
|
+
input;
|
|
182
|
+
output;
|
|
183
|
+
lineBuffer = "";
|
|
184
|
+
pendingLines = [];
|
|
185
|
+
lineWaiter = null;
|
|
186
|
+
inputEnded = false;
|
|
187
|
+
onInputData = (chunk) => {
|
|
188
|
+
this.onData(chunk);
|
|
189
|
+
};
|
|
190
|
+
onInputEnd = () => {
|
|
191
|
+
this.inputEnded = true;
|
|
192
|
+
if (this.lineWaiter != null) {
|
|
193
|
+
const waiter = this.lineWaiter;
|
|
194
|
+
this.lineWaiter = null;
|
|
195
|
+
waiter(null);
|
|
196
|
+
}
|
|
197
|
+
if (this.keyHandler != null) {
|
|
198
|
+
this.keyHandler(KEY_CTRL_D);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
/** Set while a key-driven prompt owns the input. */
|
|
202
|
+
keyHandler = null;
|
|
203
|
+
interactive;
|
|
204
|
+
style;
|
|
205
|
+
symbols;
|
|
206
|
+
/** Stops reading so the process can exit once the command is done. */
|
|
207
|
+
close() {
|
|
208
|
+
if (this.interactive == true) {
|
|
209
|
+
this.input.setRawMode?.(false);
|
|
210
|
+
this.output.write(`${ESC}[?25h`);
|
|
211
|
+
}
|
|
212
|
+
this.input.pause();
|
|
213
|
+
this.input.off("data", this.onInputData);
|
|
214
|
+
this.input.off("end", this.onInputEnd);
|
|
215
|
+
}
|
|
216
|
+
takePendingInput() {
|
|
217
|
+
const text = this.pendingLines.map((line) => line + "\n").join("") + this.lineBuffer;
|
|
218
|
+
this.pendingLines.length = 0;
|
|
219
|
+
this.lineBuffer = "";
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
|
|
224
|
+
* TTY: a numbered list, answered by number, key or label.
|
|
225
|
+
*
|
|
226
|
+
* @throws {PromptBackError} On Esc without a `cancelValue`.
|
|
227
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
228
|
+
*/
|
|
229
|
+
async select(question, items, options) {
|
|
230
|
+
const choices = items.filter((item) => "value" in item);
|
|
231
|
+
if (choices.length === 0) {
|
|
232
|
+
throw new Error("select needs at least one choice");
|
|
233
|
+
}
|
|
234
|
+
let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
|
|
235
|
+
if (startIndex < 0) {
|
|
236
|
+
startIndex = items.findIndex((item) => "value" in item);
|
|
237
|
+
}
|
|
238
|
+
this.input.resume();
|
|
239
|
+
let picked;
|
|
240
|
+
if (this.interactive == true) {
|
|
241
|
+
picked = await this.selectWithKeys(question, items, startIndex);
|
|
242
|
+
} else {
|
|
243
|
+
picked = await this.selectWithLines(question, items, startIndex);
|
|
244
|
+
}
|
|
245
|
+
if (picked != null) {
|
|
246
|
+
return picked;
|
|
247
|
+
}
|
|
248
|
+
if (options?.cancelValue != null) {
|
|
249
|
+
return options.cancelValue;
|
|
250
|
+
}
|
|
251
|
+
throw new PromptBackError();
|
|
252
|
+
}
|
|
253
|
+
readLine() {
|
|
254
|
+
const queued = this.pendingLines.shift();
|
|
255
|
+
if (queued != null) {
|
|
256
|
+
return Promise.resolve(queued);
|
|
257
|
+
}
|
|
258
|
+
if (this.inputEnded == true) {
|
|
259
|
+
return Promise.resolve(null);
|
|
260
|
+
}
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
this.lineWaiter = resolve;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
onData(chunk) {
|
|
266
|
+
if (this.keyHandler != null) {
|
|
267
|
+
for (const key of splitKeys(chunk)) {
|
|
268
|
+
if (this.keyHandler == null) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
this.keyHandler(key);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.lineBuffer += chunk;
|
|
276
|
+
let newlineIndex = this.lineBuffer.indexOf("\n");
|
|
277
|
+
while (newlineIndex >= 0) {
|
|
278
|
+
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
279
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
280
|
+
if (this.lineWaiter != null) {
|
|
281
|
+
const waiter = this.lineWaiter;
|
|
282
|
+
this.lineWaiter = null;
|
|
283
|
+
waiter(line);
|
|
284
|
+
} else {
|
|
285
|
+
this.pendingLines.push(line);
|
|
286
|
+
}
|
|
287
|
+
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async selectWithLines(question, items, startIndex) {
|
|
291
|
+
const choices = items.filter((item) => "value" in item);
|
|
292
|
+
const labelWidths = groupLabelWidths(items);
|
|
293
|
+
this.output.write(`${this.symbols.question} ${question}
|
|
294
|
+
`);
|
|
295
|
+
let number = 0;
|
|
296
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
297
|
+
const item = items[index];
|
|
298
|
+
if ("value" in item == false) {
|
|
299
|
+
this.output.write("\n");
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
number += 1;
|
|
303
|
+
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
304
|
+
if (item.hint != null && item.hint !== "") {
|
|
305
|
+
line += ` ${item.hint}`;
|
|
306
|
+
}
|
|
307
|
+
this.output.write(`${line.trimEnd()}
|
|
308
|
+
`);
|
|
309
|
+
}
|
|
310
|
+
const defaultChoice = items[startIndex];
|
|
311
|
+
const defaultNumber = choices.indexOf(defaultChoice) + 1;
|
|
312
|
+
for (; ; ) {
|
|
313
|
+
this.output.write(`Choice (${defaultNumber}): `);
|
|
314
|
+
const line = await this.readLine();
|
|
315
|
+
if (line == null) {
|
|
316
|
+
throw new PromptClosedError();
|
|
317
|
+
}
|
|
318
|
+
if (line === ESC) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const answer = line.trim();
|
|
322
|
+
if (answer === "") {
|
|
323
|
+
return defaultChoice.value;
|
|
324
|
+
}
|
|
325
|
+
const index = Number(answer);
|
|
326
|
+
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
327
|
+
return choices[index - 1].value;
|
|
328
|
+
}
|
|
329
|
+
const lowered = answer.toLowerCase();
|
|
330
|
+
const matched = choices.find((choice) => {
|
|
331
|
+
const key = choice.key ?? choice.value;
|
|
332
|
+
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
333
|
+
});
|
|
334
|
+
if (matched != null) {
|
|
335
|
+
return matched.value;
|
|
336
|
+
}
|
|
337
|
+
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ── TTY rendering ──────────────────────────────────────────────────────
|
|
342
|
+
columns() {
|
|
343
|
+
const columns = this.output.columns;
|
|
344
|
+
if (columns == null || columns < 20) {
|
|
345
|
+
return 80;
|
|
346
|
+
}
|
|
347
|
+
return columns;
|
|
348
|
+
}
|
|
349
|
+
/** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
|
|
350
|
+
fitLine(parts, width) {
|
|
351
|
+
let used = 0;
|
|
352
|
+
let line = "";
|
|
353
|
+
for (const part of parts) {
|
|
354
|
+
let text = "";
|
|
355
|
+
for (const character of part.text) {
|
|
356
|
+
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
357
|
+
if (used + characterWidth > width) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
used += characterWidth;
|
|
361
|
+
text += character;
|
|
362
|
+
}
|
|
363
|
+
if (part.paint != null) {
|
|
364
|
+
line += part.paint(text);
|
|
365
|
+
} else {
|
|
366
|
+
line += text;
|
|
367
|
+
}
|
|
368
|
+
if (used >= width) {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return line;
|
|
373
|
+
}
|
|
374
|
+
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
375
|
+
writeAnswered(question, answer) {
|
|
376
|
+
if (this.interactive == false) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
380
|
+
`);
|
|
381
|
+
}
|
|
382
|
+
/** @returns The picked value, or null on Esc. */
|
|
383
|
+
selectWithKeys(question, items, startIndex) {
|
|
384
|
+
const ttyInput = this.input;
|
|
385
|
+
const selectableIndexes = items.flatMap((item, index) => {
|
|
386
|
+
if ("value" in item) {
|
|
387
|
+
return [index];
|
|
388
|
+
}
|
|
389
|
+
return [];
|
|
390
|
+
});
|
|
391
|
+
const labelWidths = groupLabelWidths(items);
|
|
392
|
+
const ruleWidth = Math.min(Math.max(...labelWidths), 24);
|
|
393
|
+
let active = startIndex;
|
|
394
|
+
let renderedLines = 0;
|
|
395
|
+
const draw = (lines) => {
|
|
396
|
+
let text = "";
|
|
397
|
+
if (renderedLines > 1) {
|
|
398
|
+
text += `${ESC}[${renderedLines - 1}A`;
|
|
399
|
+
}
|
|
400
|
+
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
401
|
+
this.output.write(text);
|
|
402
|
+
renderedLines = lines.length;
|
|
403
|
+
};
|
|
404
|
+
const buildLines = () => {
|
|
405
|
+
const width = this.columns() - 1;
|
|
406
|
+
const lines = [
|
|
407
|
+
this.fitLine([
|
|
408
|
+
{ text: this.symbols.question, paint: this.style.accent },
|
|
409
|
+
{ text: " " },
|
|
410
|
+
{ text: question, paint: this.style.bold }
|
|
411
|
+
], width)
|
|
412
|
+
];
|
|
413
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
414
|
+
const item = items[index];
|
|
415
|
+
if ("value" in item == false) {
|
|
416
|
+
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const isActive = index === active;
|
|
420
|
+
const parts = [];
|
|
421
|
+
if (isActive == true) {
|
|
422
|
+
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
423
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
424
|
+
} else {
|
|
425
|
+
parts.push({ text: " " });
|
|
426
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
427
|
+
}
|
|
428
|
+
if (item.hint != null && item.hint !== "") {
|
|
429
|
+
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
430
|
+
}
|
|
431
|
+
lines.push(this.fitLine(parts, width));
|
|
432
|
+
}
|
|
433
|
+
lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
|
|
434
|
+
return lines;
|
|
435
|
+
};
|
|
436
|
+
const move = (step) => {
|
|
437
|
+
const position = selectableIndexes.indexOf(active);
|
|
438
|
+
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
439
|
+
active = selectableIndexes[next];
|
|
440
|
+
};
|
|
441
|
+
return new Promise((resolve, reject) => {
|
|
442
|
+
const finish = () => {
|
|
443
|
+
this.keyHandler = null;
|
|
444
|
+
ttyInput.setRawMode?.(false);
|
|
445
|
+
draw([]);
|
|
446
|
+
this.output.write(`${ESC}[?25h`);
|
|
447
|
+
};
|
|
448
|
+
this.keyHandler = (key) => {
|
|
449
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
450
|
+
finish();
|
|
451
|
+
reject(new PromptClosedError());
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
455
|
+
const picked = items[active];
|
|
456
|
+
finish();
|
|
457
|
+
this.writeAnswered(question, picked.label);
|
|
458
|
+
resolve(picked.value);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (key === ESC) {
|
|
462
|
+
finish();
|
|
463
|
+
resolve(null);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (KEY_UP.includes(key) == true) {
|
|
467
|
+
move(-1);
|
|
468
|
+
} else if (KEY_DOWN.includes(key) == true) {
|
|
469
|
+
move(1);
|
|
470
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
471
|
+
active = selectableIndexes[0];
|
|
472
|
+
} else if (KEY_END.includes(key) == true) {
|
|
473
|
+
active = selectableIndexes[selectableIndexes.length - 1];
|
|
474
|
+
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
475
|
+
active = selectableIndexes[Number(key) - 1];
|
|
476
|
+
} else {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
draw(buildLines());
|
|
480
|
+
};
|
|
481
|
+
ttyInput.setRawMode?.(true);
|
|
482
|
+
this.output.write(`${ESC}[?25l`);
|
|
483
|
+
draw(buildLines());
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/modules/launcher/menu.ts
|
|
489
|
+
async function runMenu(options) {
|
|
490
|
+
let targetId = options.targetId;
|
|
491
|
+
let defaultValue = "db";
|
|
492
|
+
for (; ; ) {
|
|
493
|
+
const prompter = new Prompter(options.input, options.output);
|
|
494
|
+
let selected = "quit";
|
|
495
|
+
try {
|
|
496
|
+
for (; ; ) {
|
|
497
|
+
let clientLabel = "Automatically selected by each MCP";
|
|
498
|
+
if (targetId != null) {
|
|
499
|
+
clientLabel = CLIENT_LABELS[targetId];
|
|
500
|
+
}
|
|
501
|
+
const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
|
|
502
|
+
{ value: "db", label: "Database", hint: "PostgreSQL connections" },
|
|
503
|
+
{ value: "ssh", label: "SSH", hint: "Remote hosts" },
|
|
504
|
+
{ value: "wsl", label: "WSL", hint: "Local Linux distros \xB7 Windows" },
|
|
505
|
+
{ separator: true },
|
|
506
|
+
{ value: "client", label: "Client", hint: clientLabel },
|
|
507
|
+
{ value: "quit", label: "Quit" }
|
|
508
|
+
], { defaultValue, cancelValue: "quit" });
|
|
509
|
+
if (picked !== "client") {
|
|
510
|
+
selected = picked;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
const clients = [
|
|
514
|
+
{ value: "auto", label: "Automatic", hint: "Let each MCP detect the client" },
|
|
515
|
+
{ value: "claude-code", label: CLIENT_LABELS["claude-code"] },
|
|
516
|
+
{ value: "codex", label: CLIENT_LABELS.codex },
|
|
517
|
+
{ value: "claude-desktop", label: CLIENT_LABELS["claude-desktop"] },
|
|
518
|
+
{ value: "cursor", label: CLIENT_LABELS.cursor },
|
|
519
|
+
{ separator: true },
|
|
520
|
+
{ value: "back", label: "Back" }
|
|
521
|
+
];
|
|
522
|
+
const client = await prompter.select("Client", clients, { defaultValue: targetId ?? "auto", cancelValue: "back" });
|
|
523
|
+
if (client === "auto") {
|
|
524
|
+
targetId = void 0;
|
|
525
|
+
} else if (client !== "back") {
|
|
526
|
+
targetId = client;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof PromptClosedError) {
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
} finally {
|
|
535
|
+
prompter.close();
|
|
536
|
+
}
|
|
537
|
+
if (selected === "quit") {
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
defaultValue = selected;
|
|
541
|
+
try {
|
|
542
|
+
const code = await options.launch({ mcpId: selected, targetId, pendingInput: prompter.takePendingInput() });
|
|
543
|
+
if (code === 130) {
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
if (code !== 0) {
|
|
547
|
+
options.output.write(`
|
|
548
|
+
Setup exited with code ${code}.
|
|
549
|
+
|
|
550
|
+
`);
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
options.output.write(`
|
|
554
|
+
${String(error)}
|
|
555
|
+
|
|
556
|
+
`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/modules/launcher/npm.ts
|
|
562
|
+
import { execFile, spawn } from "child_process";
|
|
563
|
+
import { existsSync } from "fs";
|
|
564
|
+
import { mkdtemp, realpath, rm } from "fs/promises";
|
|
565
|
+
import { tmpdir } from "os";
|
|
566
|
+
import { basename, delimiter, dirname, join } from "path";
|
|
567
|
+
import { promisify } from "util";
|
|
568
|
+
var execFileAsync = promisify(execFile);
|
|
569
|
+
async function findNpmCli() {
|
|
570
|
+
const candidates = [];
|
|
571
|
+
const npmExecPath = process.env.npm_execpath;
|
|
572
|
+
if (npmExecPath != null && ["npm-cli.js", "npx-cli.js"].includes(basename(npmExecPath)) == true) {
|
|
573
|
+
candidates.push(join(dirname(npmExecPath), "npm-cli.js"));
|
|
574
|
+
}
|
|
575
|
+
candidates.push(join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"));
|
|
576
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
577
|
+
if (directory === "") {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (process.platform === "win32") {
|
|
581
|
+
candidates.push(join(directory, "node_modules", "npm", "bin", "npm-cli.js"));
|
|
582
|
+
} else {
|
|
583
|
+
candidates.push(join(directory, "npm"));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
for (const candidate of candidates) {
|
|
587
|
+
if (existsSync(candidate) == true) {
|
|
588
|
+
const resolved = await realpath(candidate);
|
|
589
|
+
return resolved;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
throw new Error("npm could not be found. Install Node.js with npm, then reopen your terminal.");
|
|
593
|
+
}
|
|
594
|
+
async function resolveLatestVersion(options) {
|
|
595
|
+
const metadataCache = await mkdtemp(join(tmpdir(), "akms-mcp-latest-"));
|
|
596
|
+
try {
|
|
597
|
+
const result = await execFileAsync(process.execPath, [
|
|
598
|
+
options.npmCli,
|
|
599
|
+
"view",
|
|
600
|
+
`${options.packageName}@latest`,
|
|
601
|
+
"version",
|
|
602
|
+
"--json",
|
|
603
|
+
"--prefer-online",
|
|
604
|
+
"--prefer-offline=false",
|
|
605
|
+
"--offline=false",
|
|
606
|
+
"--fetch-retries=0",
|
|
607
|
+
"--fetch-timeout=15000",
|
|
608
|
+
"--update-notifier=false",
|
|
609
|
+
"--cache",
|
|
610
|
+
metadataCache
|
|
611
|
+
], { signal: options.signal, timeout: 2e4, windowsHide: true });
|
|
612
|
+
const version = JSON.parse(result.stdout);
|
|
613
|
+
if (typeof version !== "string" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) == false) {
|
|
614
|
+
throw new Error("npm returned an invalid latest version.");
|
|
615
|
+
}
|
|
616
|
+
return version;
|
|
617
|
+
} finally {
|
|
618
|
+
await rm(metadataCache, { recursive: true, force: true });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function launchMcp(options) {
|
|
622
|
+
const selected = MCP_PACKAGES[options.mcpId];
|
|
623
|
+
if (options.mcpId === "wsl" && process.platform !== "win32") {
|
|
624
|
+
throw new Error("WSL setup must run from Windows.");
|
|
625
|
+
}
|
|
626
|
+
const npmCli = await findNpmCli();
|
|
627
|
+
const cancellation = new AbortController();
|
|
628
|
+
const cancelLookup = () => {
|
|
629
|
+
cancellation.abort();
|
|
630
|
+
};
|
|
631
|
+
process.on("SIGINT", cancelLookup);
|
|
632
|
+
process.on("SIGTERM", cancelLookup);
|
|
633
|
+
let version;
|
|
634
|
+
try {
|
|
635
|
+
process.stdout.write(`
|
|
636
|
+
Checking the latest ${selected.packageName} release...
|
|
637
|
+
`);
|
|
638
|
+
version = await resolveLatestVersion({ npmCli, packageName: selected.packageName, signal: cancellation.signal });
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (cancellation.signal.aborted == true) {
|
|
641
|
+
return 130;
|
|
642
|
+
}
|
|
643
|
+
throw new Error(`Could not check the latest ${selected.packageName} release; setup was not started.
|
|
644
|
+
${String(error)}`, { cause: error });
|
|
645
|
+
} finally {
|
|
646
|
+
process.off("SIGINT", cancelLookup);
|
|
647
|
+
process.off("SIGTERM", cancelLookup);
|
|
648
|
+
}
|
|
649
|
+
const argumentsList = [
|
|
650
|
+
npmCli,
|
|
651
|
+
"exec",
|
|
652
|
+
"--yes",
|
|
653
|
+
"--prefer-online",
|
|
654
|
+
"--prefer-offline=false",
|
|
655
|
+
"--offline=false",
|
|
656
|
+
`--package=${selected.packageName}@${version}`,
|
|
657
|
+
"--",
|
|
658
|
+
selected.command,
|
|
659
|
+
selected.setupCommand
|
|
660
|
+
];
|
|
661
|
+
if (options.targetId != null) {
|
|
662
|
+
argumentsList.push("--target", options.targetId);
|
|
663
|
+
}
|
|
664
|
+
process.stdout.write(`Opening ${selected.packageName} ${version}
|
|
665
|
+
|
|
666
|
+
`);
|
|
667
|
+
return new Promise((resolve, reject) => {
|
|
668
|
+
const interactiveInput = process.stdin.isTTY === true;
|
|
669
|
+
let inputMode = "pipe";
|
|
670
|
+
if (interactiveInput == true) {
|
|
671
|
+
inputMode = "inherit";
|
|
672
|
+
}
|
|
673
|
+
const child = spawn(process.execPath, argumentsList, {
|
|
674
|
+
stdio: [inputMode, "inherit", "inherit"],
|
|
675
|
+
windowsHide: true
|
|
676
|
+
});
|
|
677
|
+
const interrupt = () => {
|
|
678
|
+
child.kill("SIGINT");
|
|
679
|
+
};
|
|
680
|
+
const terminate = () => {
|
|
681
|
+
child.kill("SIGTERM");
|
|
682
|
+
};
|
|
683
|
+
process.on("SIGINT", interrupt);
|
|
684
|
+
process.on("SIGTERM", terminate);
|
|
685
|
+
if (child.stdin != null) {
|
|
686
|
+
child.stdin.on("error", (error) => {
|
|
687
|
+
if (error.code !== "EPIPE" && error.code !== "ERR_STREAM_DESTROYED") {
|
|
688
|
+
reject(error);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
if (options.pendingInput != null) {
|
|
692
|
+
child.stdin.write(options.pendingInput);
|
|
693
|
+
}
|
|
694
|
+
process.stdin.pipe(child.stdin);
|
|
695
|
+
}
|
|
696
|
+
const cleanup = () => {
|
|
697
|
+
process.off("SIGINT", interrupt);
|
|
698
|
+
process.off("SIGTERM", terminate);
|
|
699
|
+
if (child.stdin != null) {
|
|
700
|
+
process.stdin.unpipe(child.stdin);
|
|
701
|
+
process.stdin.pause();
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
child.once("error", (error) => {
|
|
705
|
+
cleanup();
|
|
706
|
+
reject(error);
|
|
707
|
+
});
|
|
708
|
+
child.once("close", (code, signal) => {
|
|
709
|
+
cleanup();
|
|
710
|
+
if (signal != null) {
|
|
711
|
+
resolve(130);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
resolve(code ?? 1);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/cli.ts
|
|
720
|
+
var HELP_TEXT = [
|
|
721
|
+
"AKMS MCP \u2014 connection setup for Database, SSH and WSL",
|
|
722
|
+
"",
|
|
723
|
+
"Usage:",
|
|
724
|
+
" akms-mcp Open the setup menu",
|
|
725
|
+
" akms-mcp [setup|config] Open the same menu",
|
|
726
|
+
" akms-mcp [setup|config] <mcp> Open db, ssh or wsl setup directly",
|
|
727
|
+
"",
|
|
728
|
+
"Options:",
|
|
729
|
+
" --target <client> claude-code | codex | claude-desktop | cursor",
|
|
730
|
+
" -h, --help Print this help",
|
|
731
|
+
" -v, --version Print the launcher version",
|
|
732
|
+
"",
|
|
733
|
+
"Each launch checks npm for the selected package's latest release.",
|
|
734
|
+
"Node.js 22+ and npm are required. WSL setup runs on Windows."
|
|
735
|
+
].join("\n");
|
|
736
|
+
async function main() {
|
|
737
|
+
const argumentsList = parseArguments(process.argv.slice(2));
|
|
738
|
+
if (argumentsList.showHelp == true) {
|
|
739
|
+
process.stdout.write(`${HELP_TEXT}
|
|
740
|
+
`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (argumentsList.showVersion == true) {
|
|
744
|
+
let version = "development";
|
|
745
|
+
if (true) {
|
|
746
|
+
version = "0.1.0";
|
|
747
|
+
}
|
|
748
|
+
process.stdout.write(`${version}
|
|
749
|
+
`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (argumentsList.mcpId != null) {
|
|
753
|
+
process.exitCode = await launchMcp({ mcpId: argumentsList.mcpId, targetId: argumentsList.targetId });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
process.exitCode = await runMenu({
|
|
757
|
+
input: process.stdin,
|
|
758
|
+
output: process.stdout,
|
|
759
|
+
targetId: argumentsList.targetId,
|
|
760
|
+
launch: launchMcp
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
main().catch((error) => {
|
|
764
|
+
process.stderr.write(`akms-mcp: ${String(error)}
|
|
765
|
+
`);
|
|
766
|
+
process.exitCode = 1;
|
|
767
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@akms/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactive launcher for configuring the latest AKMS database, SSH and WSL MCP servers.",
|
|
5
|
+
"keywords": ["mcp", "model-context-protocol", "cli", "setup", "akms"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"akms-mcp": "lib/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc --noEmit && tsup",
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"start": "node lib/cli.js",
|
|
21
|
+
"pretest": "npm run build",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"prepack": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.19.0",
|
|
27
|
+
"tsup": "^8.5.1",
|
|
28
|
+
"tsx": "^4.23.15",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^4.1.11"
|
|
31
|
+
}
|
|
32
|
+
}
|