@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/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
- if (process.argv[2] === "install") {
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: "0.3.0" });
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 prefix = process.argv[2] === "install" ? "jira-mcp install failed" : "jira-mcp failed to start";
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 { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
- import { homedir, platform } from "node:os";
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
- const env = hasCredentials ? buildEnvFromFlags(flags) : await promptCredentials(p);
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 resolveAgents(spec) {
294
- const norm = spec.trim().toLowerCase();
295
- if (norm === "all" || norm === "a")
296
- return [...AGENTS];
297
- const ids = norm.split(/[,\s]+/).filter(Boolean);
298
- const selected = ids.map((id) => {
299
- const agent = /^\d+$/.test(id) ? AGENTS[Number(id) - 1] : AGENTS.find((a) => a.id === id);
300
- if (!agent)
301
- throw new Error(`Unknown agent '${id}'. Valid: ${AGENTS.map((a, i) => `${i + 1}/${a.id}`).join(", ")} or all`);
302
- return agent;
303
- });
304
- if (selected.length === 0)
305
- throw new Error("No agents selected");
306
- return selected;
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
+ }