@thammarongg/jira-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -11
- package/dist/adf.js +71 -0
- package/dist/agents.js +412 -0
- package/dist/client.js +6 -1
- package/dist/config.js +1 -1
- package/dist/index.js +54 -3
- package/dist/install.js +47 -381
- package/dist/prompt.js +139 -0
- package/dist/status.js +81 -0
- package/dist/tools/boards.js +3 -3
- package/dist/tools/epics.js +30 -12
- package/dist/tools/issues.js +130 -32
- package/dist/tools/projects.js +4 -2
- package/dist/tools/sprints.js +41 -23
- package/dist/tools/users.js +1 -1
- package/dist/uninstall.js +140 -0
- package/dist/update.js +71 -0
- package/package.json +1 -1
- package/skill/SKILL.md +53 -5
package/dist/index.js
CHANGED
|
@@ -12,14 +12,62 @@ import { registerUserTools } from "./tools/users.js";
|
|
|
12
12
|
import { registerMetaTools } from "./tools/meta.js";
|
|
13
13
|
import { registerGenericTools } from "./tools/generic.js";
|
|
14
14
|
import { runInstaller } from "./install.js";
|
|
15
|
+
import { runUpdate } from "./update.js";
|
|
16
|
+
import { runUninstall } from "./uninstall.js";
|
|
17
|
+
import { PACKAGE_VERSION } from "./agents.js";
|
|
18
|
+
import { renderStatus } from "./status.js";
|
|
19
|
+
function helpText() {
|
|
20
|
+
return `jira-mcp — Jira MCP server and configuration helper
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
install Configure the jira MCP server in one or more agents
|
|
24
|
+
update Refresh existing jira entries from their own credentials
|
|
25
|
+
uninstall Remove jira entries from selected agents
|
|
26
|
+
--version, -v Print the installed version
|
|
27
|
+
--help, -h Show this help
|
|
28
|
+
|
|
29
|
+
With no arguments in a terminal, jira-mcp prints a configuration status card.
|
|
30
|
+
With no arguments over piped stdio, it starts the MCP server.
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(helpText());
|
|
35
|
+
}
|
|
15
36
|
async function main() {
|
|
16
|
-
|
|
37
|
+
const command = process.argv[2];
|
|
38
|
+
if (command === "install") {
|
|
17
39
|
await runInstaller(process.argv.slice(3));
|
|
18
40
|
return;
|
|
19
41
|
}
|
|
42
|
+
if (command === "update") {
|
|
43
|
+
await runUpdate(process.argv.slice(3));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (command === "uninstall") {
|
|
47
|
+
await runUninstall(process.argv.slice(3));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (command === "--version" || command === "-v") {
|
|
51
|
+
console.log(PACKAGE_VERSION);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (command === "--help" || command === "-h") {
|
|
55
|
+
printHelp();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (command !== undefined) {
|
|
59
|
+
console.error(`Unknown command: ${command}`);
|
|
60
|
+
console.error(helpText());
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!command && process.stdin.isTTY) {
|
|
65
|
+
console.log(await renderStatus());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
20
68
|
const config = loadConfig();
|
|
21
69
|
const client = new JiraClient(config);
|
|
22
|
-
const server = new McpServer({ name: "jira", version:
|
|
70
|
+
const server = new McpServer({ name: "jira", version: PACKAGE_VERSION });
|
|
23
71
|
registerBoardTools(server, client);
|
|
24
72
|
registerSprintTools(server, client);
|
|
25
73
|
registerEpicTools(server, client);
|
|
@@ -34,7 +82,10 @@ async function main() {
|
|
|
34
82
|
}
|
|
35
83
|
main().catch((err) => {
|
|
36
84
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
-
const
|
|
85
|
+
const command = process.argv[2];
|
|
86
|
+
const prefix = command === "install" || command === "update" || command === "uninstall"
|
|
87
|
+
? `jira-mcp ${command} failed`
|
|
88
|
+
: "jira-mcp failed to start";
|
|
38
89
|
console.error(`${prefix}: ${msg}`);
|
|
39
90
|
process.exit(1);
|
|
40
91
|
});
|
package/dist/install.js
CHANGED
|
@@ -1,30 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import * as readline from "node:readline";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
function readPackageName() {
|
|
7
|
-
try {
|
|
8
|
-
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
9
|
-
const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
10
|
-
if (typeof parsed.name === "string" && parsed.name.length > 0)
|
|
11
|
-
return parsed.name;
|
|
12
|
-
}
|
|
13
|
-
catch { }
|
|
14
|
-
return "@thammarongg/jira-mcp";
|
|
15
|
-
}
|
|
16
|
-
const PKG = readPackageName();
|
|
17
|
-
const claudeDesktopPath = platform() === "darwin"
|
|
18
|
-
? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
19
|
-
: join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
20
|
-
const AGENTS = [
|
|
21
|
-
{ id: "claude-code", label: "Claude Code", path: join(homedir(), ".claude.json"), kind: "mcpServers" },
|
|
22
|
-
{ id: "opencode", label: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json"), kind: "opencode" },
|
|
23
|
-
{ id: "codex", label: "Codex", path: join(homedir(), ".codex", "config.toml"), kind: "codex" },
|
|
24
|
-
{ id: "cursor", label: "Cursor", path: join(homedir(), ".cursor", "mcp.json"), kind: "mcpServers" },
|
|
25
|
-
{ id: "claude-desktop", label: "Claude Desktop", path: claudeDesktopPath, kind: "mcpServers" },
|
|
26
|
-
{ id: "gemini-cli", label: "Gemini CLI", path: join(homedir(), ".gemini", "settings.json"), kind: "mcpServers" },
|
|
27
|
-
];
|
|
1
|
+
import { AGENTS, PKG, readAgentEnv, resolveAgents, writeAgentConfig } from "./agents.js";
|
|
2
|
+
import { askPassword, makePrompter } from "./prompt.js";
|
|
28
3
|
function parseFlags(argv) {
|
|
29
4
|
const flags = {};
|
|
30
5
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -99,7 +74,7 @@ Usage:
|
|
|
99
74
|
|
|
100
75
|
Options:
|
|
101
76
|
--agents <list> Comma-separated agent ids or 'all': ${AGENTS.map((a) => a.id).join(", ")}
|
|
102
|
-
--base-url <url> Jira base URL (required)
|
|
77
|
+
--base-url <url> Jira base URL (required unless reusing an existing entry)
|
|
103
78
|
--email <email> Jira Cloud email (with --token)
|
|
104
79
|
--username <user> Jira Data Center username (with --token or --password)
|
|
105
80
|
--token <token> API token / PAT
|
|
@@ -108,154 +83,43 @@ Options:
|
|
|
108
83
|
-h, --help Show this help
|
|
109
84
|
`);
|
|
110
85
|
}
|
|
111
|
-
function makePrompter() {
|
|
112
|
-
const queue = [];
|
|
113
|
-
const waiters = [];
|
|
114
|
-
let closed = false;
|
|
115
|
-
let suspended = false;
|
|
116
|
-
let rl = null;
|
|
117
|
-
const attach = (r) => {
|
|
118
|
-
r.on("line", (line) => {
|
|
119
|
-
const waiter = waiters.shift();
|
|
120
|
-
if (waiter)
|
|
121
|
-
waiter(line);
|
|
122
|
-
else
|
|
123
|
-
queue.push(line);
|
|
124
|
-
});
|
|
125
|
-
r.on("close", () => {
|
|
126
|
-
if (suspended)
|
|
127
|
-
return;
|
|
128
|
-
closed = true;
|
|
129
|
-
while (waiters.length > 0)
|
|
130
|
-
waiters.shift()("");
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
const start = () => {
|
|
134
|
-
rl = readline.createInterface({
|
|
135
|
-
input: process.stdin,
|
|
136
|
-
output: process.stdout,
|
|
137
|
-
terminal: process.stdin.isTTY === true,
|
|
138
|
-
});
|
|
139
|
-
attach(rl);
|
|
140
|
-
};
|
|
141
|
-
start();
|
|
142
|
-
const prompter = {
|
|
143
|
-
get closed() {
|
|
144
|
-
return closed;
|
|
145
|
-
},
|
|
146
|
-
ask(question) {
|
|
147
|
-
process.stdout.write(question);
|
|
148
|
-
const queued = queue.shift();
|
|
149
|
-
if (queued !== undefined)
|
|
150
|
-
return Promise.resolve(queued);
|
|
151
|
-
if (closed)
|
|
152
|
-
return Promise.resolve("");
|
|
153
|
-
return new Promise((resolve) => waiters.push(resolve));
|
|
154
|
-
},
|
|
155
|
-
close() {
|
|
156
|
-
suspended = false;
|
|
157
|
-
closed = true;
|
|
158
|
-
while (waiters.length > 0)
|
|
159
|
-
waiters.shift()("");
|
|
160
|
-
rl?.close();
|
|
161
|
-
rl = null;
|
|
162
|
-
},
|
|
163
|
-
suspend() {
|
|
164
|
-
suspended = true;
|
|
165
|
-
rl?.close();
|
|
166
|
-
rl = null;
|
|
167
|
-
},
|
|
168
|
-
resume() {
|
|
169
|
-
suspended = false;
|
|
170
|
-
if (rl === null)
|
|
171
|
-
start();
|
|
172
|
-
},
|
|
173
|
-
tryQueued() {
|
|
174
|
-
return queue.shift();
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
return prompter;
|
|
178
|
-
}
|
|
179
|
-
function askPassword(p, promptText) {
|
|
180
|
-
const stdin = process.stdin;
|
|
181
|
-
if (typeof stdin.setRawMode !== "function") {
|
|
182
|
-
return p.ask(`${promptText} (no echo available) `).then((v) => v.trim());
|
|
183
|
-
}
|
|
184
|
-
const queued = p.tryQueued();
|
|
185
|
-
if (queued !== undefined) {
|
|
186
|
-
process.stdout.write(promptText);
|
|
187
|
-
return Promise.resolve(queued.trim());
|
|
188
|
-
}
|
|
189
|
-
return new Promise((resolve, reject) => {
|
|
190
|
-
let value = "";
|
|
191
|
-
let finished = false;
|
|
192
|
-
const cleanup = () => {
|
|
193
|
-
if (finished)
|
|
194
|
-
return;
|
|
195
|
-
finished = true;
|
|
196
|
-
stdin.removeListener("data", onData);
|
|
197
|
-
stdin.removeListener("end", onEnd);
|
|
198
|
-
stdin.removeListener("close", onClose);
|
|
199
|
-
stdin.setRawMode(false);
|
|
200
|
-
};
|
|
201
|
-
const finish = (v) => {
|
|
202
|
-
cleanup();
|
|
203
|
-
p.resume();
|
|
204
|
-
if (v === null)
|
|
205
|
-
reject(new Error("input closed while reading password"));
|
|
206
|
-
else {
|
|
207
|
-
process.stdout.write("\n");
|
|
208
|
-
resolve(v.trim());
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const onData = (buf) => {
|
|
212
|
-
for (const ch of buf.toString("utf8")) {
|
|
213
|
-
if (ch === "\r" || ch === "\n") {
|
|
214
|
-
finish(value);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (ch === "\x03") {
|
|
218
|
-
cleanup();
|
|
219
|
-
p.resume();
|
|
220
|
-
process.exit(130);
|
|
221
|
-
}
|
|
222
|
-
if (ch === "\x04") {
|
|
223
|
-
finish("");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (ch === "\u007f" || ch === "\b") {
|
|
227
|
-
if (value.length > 0) {
|
|
228
|
-
value = value.slice(0, -1);
|
|
229
|
-
process.stdout.write("\b \b");
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
else if (ch >= " ") {
|
|
233
|
-
value += ch;
|
|
234
|
-
process.stdout.write("*");
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const onEnd = () => finish(null);
|
|
239
|
-
const onClose = () => finish(null);
|
|
240
|
-
p.suspend();
|
|
241
|
-
process.stdout.write(promptText);
|
|
242
|
-
stdin.setRawMode(true);
|
|
243
|
-
stdin.resume();
|
|
244
|
-
stdin.on("data", onData);
|
|
245
|
-
stdin.on("end", onEnd);
|
|
246
|
-
stdin.on("close", onClose);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
86
|
export async function runInstaller(argv) {
|
|
250
87
|
const flags = parseFlags(argv);
|
|
251
88
|
validateCredentialFlags(flags);
|
|
252
|
-
const hasCredentials = (flags.baseUrl && flags.email && flags.token) ||
|
|
253
|
-
(flags.baseUrl && flags.username && (flags.token || flags.password));
|
|
89
|
+
const hasCredentials = Boolean((flags.baseUrl && flags.email && flags.token) ||
|
|
90
|
+
(flags.baseUrl && flags.username && (flags.token || flags.password)));
|
|
254
91
|
const needsPrompts = !flags.agents || !hasCredentials || !flags.yes;
|
|
255
92
|
const p = needsPrompts ? makePrompter() : undefined;
|
|
256
93
|
try {
|
|
257
94
|
const selected = flags.agents ? resolveAgents(flags.agents) : await promptSelection(p);
|
|
258
|
-
|
|
95
|
+
let env;
|
|
96
|
+
if (hasCredentials) {
|
|
97
|
+
env = buildEnvFromFlags(flags);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const reusable = findReusableCredentials(selected);
|
|
101
|
+
if (reusable) {
|
|
102
|
+
const summary = credentialSummary(reusable.env);
|
|
103
|
+
console.log(`\nFound existing jira credentials in ${reusable.agent.label}: ${summary}`);
|
|
104
|
+
if (flags.yes) {
|
|
105
|
+
console.log(`Using existing credentials from ${reusable.agent.label}.`);
|
|
106
|
+
env = reusable.env;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const answer = (await p.ask(`Reuse existing credentials for ${summary}? [Y/n] `)).trim().toLowerCase();
|
|
110
|
+
if (answer === "" || answer === "y" || answer === "yes")
|
|
111
|
+
env = reusable.env;
|
|
112
|
+
else
|
|
113
|
+
env = await promptCredentials(p);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (flags.yes) {
|
|
118
|
+
throw new Error("No existing Jira credentials found; provide credential flags or run install interactively");
|
|
119
|
+
}
|
|
120
|
+
env = await promptCredentials(p);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
259
123
|
console.log("\nWill configure the jira MCP server in:");
|
|
260
124
|
for (const agent of selected)
|
|
261
125
|
console.log(` - ${agent.label.padEnd(15)} ${agent.path}`);
|
|
@@ -290,20 +154,20 @@ export async function runInstaller(argv) {
|
|
|
290
154
|
p?.close();
|
|
291
155
|
}
|
|
292
156
|
}
|
|
293
|
-
function
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
157
|
+
function findReusableCredentials(selected) {
|
|
158
|
+
for (const agent of AGENTS) {
|
|
159
|
+
if (!selected.includes(agent))
|
|
160
|
+
continue;
|
|
161
|
+
const env = readAgentEnv(agent);
|
|
162
|
+
if (env)
|
|
163
|
+
return { agent, env };
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
function credentialSummary(env) {
|
|
168
|
+
const baseUrl = env.JIRA_BASE_URL ?? "unknown URL";
|
|
169
|
+
const account = env.JIRA_EMAIL ?? env.JIRA_USERNAME ?? "unknown account";
|
|
170
|
+
return `${baseUrl} (${account})`;
|
|
307
171
|
}
|
|
308
172
|
function buildEnvFromFlags(flags) {
|
|
309
173
|
const env = { JIRA_BASE_URL: flags.baseUrl };
|
|
@@ -364,201 +228,3 @@ async function promptCredentials(p) {
|
|
|
364
228
|
}
|
|
365
229
|
return env;
|
|
366
230
|
}
|
|
367
|
-
function writeAgentConfig(agent, env) {
|
|
368
|
-
if (agent.kind === "codex")
|
|
369
|
-
return upsertCodex(agent.path, env);
|
|
370
|
-
if (agent.kind === "opencode") {
|
|
371
|
-
const jsonc = agent.path.replace(/\.json$/, ".jsonc");
|
|
372
|
-
if (existsSync(jsonc)) {
|
|
373
|
-
console.log(` ! ${jsonc} exists — skipping automatic opencode write (JSONC may contain comments); merge the jira entry manually`);
|
|
374
|
-
return "skipped";
|
|
375
|
-
}
|
|
376
|
-
return mergeJsonFile(agent.path, (obj) => {
|
|
377
|
-
const mcp = (obj.mcp ??= {});
|
|
378
|
-
reportEnvChanges(envOf(mcp.jira), env, agent.path);
|
|
379
|
-
mcp.jira = {
|
|
380
|
-
type: "local",
|
|
381
|
-
command: ["npx", "-y", PKG],
|
|
382
|
-
environment: env,
|
|
383
|
-
enabled: true,
|
|
384
|
-
};
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
return mergeJsonFile(agent.path, (obj) => {
|
|
388
|
-
const mcpServers = (obj.mcpServers ??= {});
|
|
389
|
-
reportEnvChanges(envOf(mcpServers.jira), env, agent.path);
|
|
390
|
-
mcpServers.jira = { command: "npx", args: ["-y", PKG], env };
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
function envOf(entry) {
|
|
394
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
395
|
-
return undefined;
|
|
396
|
-
const e = entry.env ?? entry.environment;
|
|
397
|
-
if (!e || typeof e !== "object" || Array.isArray(e))
|
|
398
|
-
return undefined;
|
|
399
|
-
return e;
|
|
400
|
-
}
|
|
401
|
-
function reportEnvChanges(oldEnv, newEnv, path) {
|
|
402
|
-
if (!oldEnv)
|
|
403
|
-
return;
|
|
404
|
-
const replaced = Object.keys(newEnv).filter((k) => oldEnv[k] !== newEnv[k]);
|
|
405
|
-
const removed = Object.keys(oldEnv).filter((k) => !(k in newEnv));
|
|
406
|
-
if (replaced.length > 0)
|
|
407
|
-
console.log(` ! ${path}: existing jira entry will be replaced — env vars changed: ${replaced.join(", ")}`);
|
|
408
|
-
if (removed.length > 0)
|
|
409
|
-
console.log(` ! ${path}: existing jira entry will be replaced — env vars removed: ${removed.join(", ")}`);
|
|
410
|
-
}
|
|
411
|
-
function fileStamp(path) {
|
|
412
|
-
const s = statSync(path);
|
|
413
|
-
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
414
|
-
}
|
|
415
|
-
function assertUnchanged(path, before) {
|
|
416
|
-
const after = statSync(path);
|
|
417
|
-
if (after.mtimeMs !== before.mtimeMs || after.size !== before.size) {
|
|
418
|
-
throw new Error("changed while installing — close running agents and retry");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
function atomicWrite(path, content) {
|
|
422
|
-
const tmp = `${path}.tmp-${process.pid}`;
|
|
423
|
-
writeFileSync(tmp, content);
|
|
424
|
-
renameSync(tmp, path);
|
|
425
|
-
}
|
|
426
|
-
function mergeJsonFile(path, mutate) {
|
|
427
|
-
const existed = existsSync(path);
|
|
428
|
-
let stamp;
|
|
429
|
-
let obj = {};
|
|
430
|
-
if (existed) {
|
|
431
|
-
stamp = fileStamp(path);
|
|
432
|
-
const raw = readFileSync(path, "utf8");
|
|
433
|
-
let parsed;
|
|
434
|
-
try {
|
|
435
|
-
parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
436
|
-
}
|
|
437
|
-
catch {
|
|
438
|
-
parsed = undefined;
|
|
439
|
-
}
|
|
440
|
-
if (parsed === undefined || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
441
|
-
console.log(` ! ${path} is not a JSON object — backing up to ${path}.bak and starting fresh`);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
obj = parsed;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
mutate(obj);
|
|
448
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
449
|
-
if (existed) {
|
|
450
|
-
assertUnchanged(path, stamp);
|
|
451
|
-
copyFileSync(path, `${path}.bak`);
|
|
452
|
-
}
|
|
453
|
-
atomicWrite(path, JSON.stringify(obj, null, 2) + "\n");
|
|
454
|
-
return existed ? "updated" : "created";
|
|
455
|
-
}
|
|
456
|
-
function tomlStr(v) {
|
|
457
|
-
let out = "";
|
|
458
|
-
for (const ch of v) {
|
|
459
|
-
const code = ch.codePointAt(0);
|
|
460
|
-
if (ch === "\\")
|
|
461
|
-
out += "\\\\";
|
|
462
|
-
else if (ch === '"')
|
|
463
|
-
out += '\\"';
|
|
464
|
-
else if (ch === "\n")
|
|
465
|
-
out += "\\n";
|
|
466
|
-
else if (ch === "\t")
|
|
467
|
-
out += "\\t";
|
|
468
|
-
else if (ch === "\r")
|
|
469
|
-
out += "\\r";
|
|
470
|
-
else if (code < 0x20)
|
|
471
|
-
out += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
472
|
-
else
|
|
473
|
-
out += ch;
|
|
474
|
-
}
|
|
475
|
-
return `"${out}"`;
|
|
476
|
-
}
|
|
477
|
-
const JIRA_HEADER = /^\s*\[mcp_servers\.jira\](\s+#.*)?$/;
|
|
478
|
-
function parseTomlEnv(lines, start) {
|
|
479
|
-
let end = lines.length;
|
|
480
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
481
|
-
if (/^\s*\[/.test(lines[i])) {
|
|
482
|
-
end = i;
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
for (let i = start + 1; i < end; i++) {
|
|
487
|
-
const m = lines[i].match(/^\s*env\s*=\s*\{(.*)\}\s*$/);
|
|
488
|
-
if (!m)
|
|
489
|
-
continue;
|
|
490
|
-
const out = {};
|
|
491
|
-
for (const part of m[1].split(",")) {
|
|
492
|
-
const eq = part.indexOf("=");
|
|
493
|
-
if (eq < 0)
|
|
494
|
-
continue;
|
|
495
|
-
const k = part.slice(0, eq).trim();
|
|
496
|
-
let v = part.slice(eq + 1).trim();
|
|
497
|
-
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
498
|
-
v = v.slice(1, -1);
|
|
499
|
-
if (k)
|
|
500
|
-
out[k] = v;
|
|
501
|
-
}
|
|
502
|
-
return out;
|
|
503
|
-
}
|
|
504
|
-
return undefined;
|
|
505
|
-
}
|
|
506
|
-
function upsertCodex(path, env) {
|
|
507
|
-
const block = [
|
|
508
|
-
"[mcp_servers.jira]",
|
|
509
|
-
'command = "npx"',
|
|
510
|
-
`args = ["-y", ${tomlStr(PKG)}]`,
|
|
511
|
-
`env = { ${Object.entries(env)
|
|
512
|
-
.map(([k, v]) => `${k} = ${tomlStr(v)}`)
|
|
513
|
-
.join(", ")} }`,
|
|
514
|
-
];
|
|
515
|
-
if (!existsSync(path)) {
|
|
516
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
517
|
-
atomicWrite(path, block.join("\n") + "\n");
|
|
518
|
-
return "created";
|
|
519
|
-
}
|
|
520
|
-
const stamp = fileStamp(path);
|
|
521
|
-
const raw = readFileSync(path, "utf8");
|
|
522
|
-
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
523
|
-
const lines = raw.split("\n").map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l));
|
|
524
|
-
const starts = [];
|
|
525
|
-
for (let i = 0; i < lines.length; i++) {
|
|
526
|
-
if (JIRA_HEADER.test(lines[i]))
|
|
527
|
-
starts.push(i);
|
|
528
|
-
}
|
|
529
|
-
let content;
|
|
530
|
-
if (starts.length === 0) {
|
|
531
|
-
content = raw.replace(/\s*$/, "") + eol + eol + block.join(eol) + eol;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
const remove = new Set();
|
|
535
|
-
for (const start of starts) {
|
|
536
|
-
let end = lines.length;
|
|
537
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
538
|
-
if (/^\s*\[/.test(lines[i])) {
|
|
539
|
-
end = i;
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
for (let i = start; i < end; i++)
|
|
544
|
-
remove.add(i);
|
|
545
|
-
}
|
|
546
|
-
const kept = lines.filter((_, i) => !remove.has(i));
|
|
547
|
-
let insertIdx = 0;
|
|
548
|
-
for (let i = 0; i < starts[0]; i++)
|
|
549
|
-
if (!remove.has(i))
|
|
550
|
-
insertIdx++;
|
|
551
|
-
kept.splice(insertIdx, 0, ...block);
|
|
552
|
-
const after = insertIdx + block.length;
|
|
553
|
-
if (after < kept.length && kept[after].trim() !== "")
|
|
554
|
-
kept.splice(after, 0, "");
|
|
555
|
-
content = kept.join(eol);
|
|
556
|
-
if (!content.endsWith(eol))
|
|
557
|
-
content += eol;
|
|
558
|
-
}
|
|
559
|
-
reportEnvChanges(starts.length > 0 ? parseTomlEnv(lines, starts[0]) : undefined, env, path);
|
|
560
|
-
assertUnchanged(path, stamp);
|
|
561
|
-
copyFileSync(path, `${path}.bak`);
|
|
562
|
-
atomicWrite(path, content);
|
|
563
|
-
return "updated";
|
|
564
|
-
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
export function makePrompter() {
|
|
3
|
+
const queue = [];
|
|
4
|
+
const waiters = [];
|
|
5
|
+
let closed = false;
|
|
6
|
+
let suspended = false;
|
|
7
|
+
let rl = null;
|
|
8
|
+
const attach = (r) => {
|
|
9
|
+
r.on("line", (line) => {
|
|
10
|
+
const waiter = waiters.shift();
|
|
11
|
+
if (waiter)
|
|
12
|
+
waiter(line);
|
|
13
|
+
else
|
|
14
|
+
queue.push(line);
|
|
15
|
+
});
|
|
16
|
+
r.on("close", () => {
|
|
17
|
+
if (suspended)
|
|
18
|
+
return;
|
|
19
|
+
closed = true;
|
|
20
|
+
while (waiters.length > 0)
|
|
21
|
+
waiters.shift()("");
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const start = () => {
|
|
25
|
+
rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
terminal: process.stdin.isTTY === true,
|
|
29
|
+
});
|
|
30
|
+
attach(rl);
|
|
31
|
+
};
|
|
32
|
+
start();
|
|
33
|
+
const prompter = {
|
|
34
|
+
get closed() {
|
|
35
|
+
return closed;
|
|
36
|
+
},
|
|
37
|
+
ask(question) {
|
|
38
|
+
process.stdout.write(question);
|
|
39
|
+
const queued = queue.shift();
|
|
40
|
+
if (queued !== undefined)
|
|
41
|
+
return Promise.resolve(queued);
|
|
42
|
+
if (closed)
|
|
43
|
+
return Promise.resolve("");
|
|
44
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
45
|
+
},
|
|
46
|
+
close() {
|
|
47
|
+
suspended = false;
|
|
48
|
+
closed = true;
|
|
49
|
+
while (waiters.length > 0)
|
|
50
|
+
waiters.shift()("");
|
|
51
|
+
rl?.close();
|
|
52
|
+
rl = null;
|
|
53
|
+
},
|
|
54
|
+
suspend() {
|
|
55
|
+
suspended = true;
|
|
56
|
+
rl?.close();
|
|
57
|
+
rl = null;
|
|
58
|
+
},
|
|
59
|
+
resume() {
|
|
60
|
+
suspended = false;
|
|
61
|
+
if (rl === null)
|
|
62
|
+
start();
|
|
63
|
+
},
|
|
64
|
+
tryQueued() {
|
|
65
|
+
return queue.shift();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return prompter;
|
|
69
|
+
}
|
|
70
|
+
export function askPassword(p, promptText) {
|
|
71
|
+
const stdin = process.stdin;
|
|
72
|
+
if (typeof stdin.setRawMode !== "function") {
|
|
73
|
+
return p.ask(`${promptText} (no echo available) `).then((v) => v.trim());
|
|
74
|
+
}
|
|
75
|
+
const queued = p.tryQueued();
|
|
76
|
+
if (queued !== undefined) {
|
|
77
|
+
process.stdout.write(promptText);
|
|
78
|
+
return Promise.resolve(queued.trim());
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
let value = "";
|
|
82
|
+
let finished = false;
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
if (finished)
|
|
85
|
+
return;
|
|
86
|
+
finished = true;
|
|
87
|
+
stdin.removeListener("data", onData);
|
|
88
|
+
stdin.removeListener("end", onEnd);
|
|
89
|
+
stdin.removeListener("close", onClose);
|
|
90
|
+
stdin.setRawMode(false);
|
|
91
|
+
};
|
|
92
|
+
const finish = (v) => {
|
|
93
|
+
cleanup();
|
|
94
|
+
p.resume();
|
|
95
|
+
if (v === null)
|
|
96
|
+
reject(new Error("input closed while reading password"));
|
|
97
|
+
else {
|
|
98
|
+
process.stdout.write("\n");
|
|
99
|
+
resolve(v.trim());
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const onData = (buf) => {
|
|
103
|
+
for (const ch of buf.toString("utf8")) {
|
|
104
|
+
if (ch === "\r" || ch === "\n") {
|
|
105
|
+
finish(value);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (ch === "\x03") {
|
|
109
|
+
cleanup();
|
|
110
|
+
p.resume();
|
|
111
|
+
process.exit(130);
|
|
112
|
+
}
|
|
113
|
+
if (ch === "\x04") {
|
|
114
|
+
finish("");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
118
|
+
if (value.length > 0) {
|
|
119
|
+
value = value.slice(0, -1);
|
|
120
|
+
process.stdout.write("\b \b");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (ch >= " ") {
|
|
124
|
+
value += ch;
|
|
125
|
+
process.stdout.write("*");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const onEnd = () => finish(null);
|
|
130
|
+
const onClose = () => finish(null);
|
|
131
|
+
p.suspend();
|
|
132
|
+
process.stdout.write(promptText);
|
|
133
|
+
stdin.setRawMode(true);
|
|
134
|
+
stdin.resume();
|
|
135
|
+
stdin.on("data", onData);
|
|
136
|
+
stdin.on("end", onEnd);
|
|
137
|
+
stdin.on("close", onClose);
|
|
138
|
+
});
|
|
139
|
+
}
|