@thammarongg/jira-mcp 0.4.0 → 0.5.1
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 +37 -9
- package/dist/agents.js +417 -0
- package/dist/index.js +54 -3
- package/dist/install.js +49 -383
- package/dist/prompt.js +139 -0
- package/dist/status.js +81 -0
- package/dist/uninstall.js +140 -0
- package/dist/update.js +71 -0
- package/package.json +1 -1
- package/skill/SKILL.md +27 -4
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_SPEC, 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++) {
|
|
@@ -94,12 +69,12 @@ function printUsage() {
|
|
|
94
69
|
console.log(`jira-mcp install — configure the jira MCP server in your agent(s)
|
|
95
70
|
|
|
96
71
|
Usage:
|
|
97
|
-
npx -y ${
|
|
98
|
-
npx -y ${
|
|
72
|
+
npx -y ${PKG_SPEC} install
|
|
73
|
+
npx -y ${PKG_SPEC} install --agents all --base-url https://x.atlassian.net --email you@x.com --token xxx --yes
|
|
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
|
+
}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AGENTS, hasAgentEntry, PACKAGE_VERSION, readAgentEnv } from "./agents.js";
|
|
2
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@thammarongg/jira-mcp/latest";
|
|
3
|
+
function accountOf(env) {
|
|
4
|
+
return env?.JIRA_EMAIL ?? env?.JIRA_USERNAME ?? "unknown account";
|
|
5
|
+
}
|
|
6
|
+
function baseUrlOf(env) {
|
|
7
|
+
return env?.JIRA_BASE_URL ?? "unknown URL";
|
|
8
|
+
}
|
|
9
|
+
function versionParts(version) {
|
|
10
|
+
const match = version.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
11
|
+
if (!match)
|
|
12
|
+
return undefined;
|
|
13
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
14
|
+
}
|
|
15
|
+
function isNewer(candidate, installed) {
|
|
16
|
+
const next = versionParts(candidate);
|
|
17
|
+
const current = versionParts(installed);
|
|
18
|
+
if (!next || !current)
|
|
19
|
+
return false;
|
|
20
|
+
for (let i = 0; i < next.length; i++) {
|
|
21
|
+
if (next[i] !== current[i])
|
|
22
|
+
return next[i] > current[i];
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
async function latestVersion() {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
let deadlineTimer;
|
|
29
|
+
try {
|
|
30
|
+
const request = fetch(REGISTRY_URL, { signal: controller.signal }).then(async (response) => {
|
|
31
|
+
if (!response.ok)
|
|
32
|
+
return undefined;
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
35
|
+
return undefined;
|
|
36
|
+
const version = data.version;
|
|
37
|
+
return typeof version === "string" && isNewer(version, PACKAGE_VERSION) ? version : undefined;
|
|
38
|
+
});
|
|
39
|
+
return await Promise.race([
|
|
40
|
+
request,
|
|
41
|
+
new Promise((_, reject) => {
|
|
42
|
+
deadlineTimer = setTimeout(() => {
|
|
43
|
+
controller.abort();
|
|
44
|
+
reject(new Error("registry request timed out"));
|
|
45
|
+
}, 2_000);
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (deadlineTimer !== undefined)
|
|
54
|
+
clearTimeout(deadlineTimer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function configuredAgents() {
|
|
58
|
+
return AGENTS.filter((agent) => hasAgentEntry(agent));
|
|
59
|
+
}
|
|
60
|
+
export async function renderStatus() {
|
|
61
|
+
const update = await latestVersion();
|
|
62
|
+
const configured = configuredAgents();
|
|
63
|
+
const configuredIds = new Set(configured.map((agent) => agent.id));
|
|
64
|
+
const notConfigured = AGENTS.filter((agent) => !configuredIds.has(agent.id));
|
|
65
|
+
const lines = [`Installed version: ${PACKAGE_VERSION}`];
|
|
66
|
+
if (update)
|
|
67
|
+
lines.push(`update available: ${PACKAGE_VERSION} -> ${update}`);
|
|
68
|
+
lines.push("", "Configured agents:");
|
|
69
|
+
if (configured.length === 0) {
|
|
70
|
+
lines.push(" none");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const agent of configured) {
|
|
74
|
+
const env = readAgentEnv(agent);
|
|
75
|
+
lines.push(` - ${agent.label}: ${agent.path} — ${baseUrlOf(env)} (${accountOf(env)})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
lines.push(`Not configured: ${notConfigured.length > 0 ? notConfigured.map((agent) => agent.label).join(", ") : "none"}`);
|
|
79
|
+
lines.push("", "Commands:", " install Configure Jira for one or more agents", " update Refresh existing Jira entries from their own credentials", " uninstall Remove Jira entries from selected agents", " --version, -v Print the installed version", " --help, -h Show command help", "", "This command is the MCP server: agents run it over stdio with JIRA_* env vars; running it bare without those variables reports a missing JIRA_BASE_URL.");
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|