mcp-for-i 0.1.13 → 0.3.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 +27 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +52 -11
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +178 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +147 -100
- package/dist/controlplane/ui.js +1783 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +334 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +93 -0
- package/dist/ibmi/tn5250/ebcdicTables.js +27 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +1033 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +79 -24
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +800 -0
- package/dist/mcp/modules/tn5250App.js +194 -0
- package/dist/mcp/modules/tn5250Font.js +8 -0
- package/dist/mcp/modules/tn5250Live.js +221 -0
- package/dist/mcp/modules/tn5250LivePage.js +480 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1719 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +264 -220
- package/docs/configuration.md +193 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +367 -188
- package/package.json +5 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import fs from "fs
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import fsp from "fs/promises";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
import { spawn } from "child_process";
|
|
@@ -229,7 +230,7 @@ export class ClientIntegrationService {
|
|
|
229
230
|
async isCodeConfigured() {
|
|
230
231
|
const userConfigPath = getVsCodeUserMcpPath();
|
|
231
232
|
try {
|
|
232
|
-
const raw = await
|
|
233
|
+
const raw = await fsp.readFile(userConfigPath, "utf8");
|
|
233
234
|
const parsed = JSON.parse(raw);
|
|
234
235
|
if (Array.isArray(parsed?.servers)) {
|
|
235
236
|
return parsed.servers.some((entry) => entry?.name === SERVER_NAME);
|
|
@@ -263,10 +264,88 @@ async function commandExists(command) {
|
|
|
263
264
|
const result = await runCommand(command, ["--help"]);
|
|
264
265
|
return result.ok;
|
|
265
266
|
}
|
|
266
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Finds a command on PATH the way cmd.exe does (PATHEXT order). Returns the
|
|
269
|
+
* full path, or undefined when nothing matches.
|
|
270
|
+
*/
|
|
271
|
+
export function resolveWindowsCommand(command, env = process.env) {
|
|
272
|
+
const pathExt = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
273
|
+
.split(";")
|
|
274
|
+
.map(ext => ext.trim().toLowerCase())
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
const hasExt = pathExt.includes(path.extname(command).toLowerCase());
|
|
277
|
+
const candidates = hasExt ? [command] : pathExt.map(ext => command + ext);
|
|
278
|
+
const isPath = command.includes("/") || command.includes("\\");
|
|
279
|
+
const dirs = isPath
|
|
280
|
+
? [""]
|
|
281
|
+
: String(env.PATH || env.Path || "").split(";").map(dir => dir.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
|
|
282
|
+
for (const dir of dirs) {
|
|
283
|
+
for (const candidate of candidates) {
|
|
284
|
+
const full = dir ? path.join(dir, candidate) : candidate;
|
|
285
|
+
try {
|
|
286
|
+
if (fs.statSync(full).isFile())
|
|
287
|
+
return full;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// not here
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
297
|
+
/** Quotes one argument for cmd.exe, following the MSVCRT argv rules. */
|
|
298
|
+
export function escapeCmdArgument(value, doubleEscapeMeta) {
|
|
299
|
+
let arg = String(value);
|
|
300
|
+
// Double the backslashes that precede a quote, then escape the quote.
|
|
301
|
+
arg = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
302
|
+
// Double trailing backslashes so they do not escape the closing quote.
|
|
303
|
+
arg = arg.replace(/(\\*)$/, "$1$1");
|
|
304
|
+
arg = `"${arg}"`;
|
|
305
|
+
arg = arg.replace(CMD_META, "^$1");
|
|
306
|
+
// .cmd shims re-parse their arguments (%*), so they need a second pass.
|
|
307
|
+
if (doubleEscapeMeta)
|
|
308
|
+
arg = arg.replace(CMD_META, "^$1");
|
|
309
|
+
return arg;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Builds how to start a command. On Windows, npm-installed CLIs (codex,
|
|
313
|
+
* claude, code) are .cmd shims that only start through cmd.exe, so they are
|
|
314
|
+
* resolved via PATHEXT and launched with every argument escaped. The
|
|
315
|
+
* arguments are fixed by this module, never taken from a request.
|
|
316
|
+
*/
|
|
317
|
+
export function buildSpawnSpec(command, args, platform = process.platform, env = process.env) {
|
|
318
|
+
if (platform !== "win32") {
|
|
319
|
+
return { file: command, args, windowsVerbatimArguments: false };
|
|
320
|
+
}
|
|
321
|
+
const resolved = resolveWindowsCommand(command, env);
|
|
322
|
+
if (!resolved)
|
|
323
|
+
return undefined;
|
|
324
|
+
if (!/\.(cmd|bat)$/i.test(resolved)) {
|
|
325
|
+
return { file: resolved, args, windowsVerbatimArguments: false };
|
|
326
|
+
}
|
|
327
|
+
const commandLine = [
|
|
328
|
+
resolved.replace(CMD_META, "^$1"),
|
|
329
|
+
...args.map(arg => escapeCmdArgument(arg, true))
|
|
330
|
+
].join(" ");
|
|
331
|
+
return {
|
|
332
|
+
file: env.ComSpec || env.COMSPEC || "cmd.exe",
|
|
333
|
+
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
334
|
+
windowsVerbatimArguments: true
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
export async function runCommand(command, args, env = process.env) {
|
|
267
338
|
return new Promise(resolve => {
|
|
268
|
-
const
|
|
339
|
+
const spec = buildSpawnSpec(command, args, process.platform, env);
|
|
340
|
+
if (!spec) {
|
|
341
|
+
resolve({ ok: false, stdout: "", stderr: `${command} was not found on PATH` });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const child = spawn(spec.file, spec.args, {
|
|
345
|
+
env,
|
|
269
346
|
windowsHide: true,
|
|
347
|
+
windowsVerbatimArguments: spec.windowsVerbatimArguments,
|
|
348
|
+
timeout: 120000,
|
|
270
349
|
stdio: ["ignore", "pipe", "pipe"]
|
|
271
350
|
});
|
|
272
351
|
const stdout = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { startControlPlaneServer } from "./server.js";
|
|
2
2
|
import { openUrl } from "./platform.js";
|
|
3
3
|
import { getControlPlaneUrl } from "./serverShared.js";
|
|
4
|
-
import { disableBackgroundService, enableBackgroundService, ensureControlPlaneRunning, getServiceStatus } from "./serviceManager.js";
|
|
4
|
+
import { disableBackgroundService, enableBackgroundService, ensureControlPlaneRunning, getServiceStatus, stopControlPlane } from "./serviceManager.js";
|
|
5
5
|
export async function runControlCommand(args, options) {
|
|
6
6
|
const stderr = options.stderr || process.stderr;
|
|
7
7
|
const command = normalizeCommand(args, options.defaultCommand || "serve");
|
|
@@ -17,6 +17,13 @@ export async function runControlCommand(args, options) {
|
|
|
17
17
|
case "open":
|
|
18
18
|
await openControlUi(stderr, scriptPath, nodePath);
|
|
19
19
|
return;
|
|
20
|
+
case "stop":
|
|
21
|
+
await stop(stderr, scriptPath, nodePath);
|
|
22
|
+
return;
|
|
23
|
+
case "restart":
|
|
24
|
+
await stop(stderr, scriptPath, nodePath);
|
|
25
|
+
await openControlUi(stderr, scriptPath, nodePath, false);
|
|
26
|
+
return;
|
|
20
27
|
case "enable":
|
|
21
28
|
await enableService(stderr, scriptPath, nodePath);
|
|
22
29
|
return;
|
|
@@ -29,25 +36,52 @@ export async function runControlCommand(args, options) {
|
|
|
29
36
|
return;
|
|
30
37
|
}
|
|
31
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs the control plane in the foreground. Resolves only after the server has
|
|
41
|
+
* stopped listening, so a caller that exits when this returns keeps it alive.
|
|
42
|
+
*/
|
|
32
43
|
async function serve(stderr) {
|
|
44
|
+
let server;
|
|
33
45
|
try {
|
|
34
|
-
|
|
35
|
-
stderr.write(`mcp-for-i control plane running at http://${server.host}:${server.port}\n`);
|
|
36
|
-
const shutdown = async () => {
|
|
37
|
-
await server.close();
|
|
38
|
-
process.exit(0);
|
|
39
|
-
};
|
|
40
|
-
process.on("SIGINT", shutdown);
|
|
41
|
-
process.on("SIGTERM", shutdown);
|
|
46
|
+
server = await startControlPlaneServer();
|
|
42
47
|
}
|
|
43
48
|
catch (err) {
|
|
44
49
|
if (err?.code === "EADDRINUSE") {
|
|
45
50
|
const url = getControlPlaneUrl();
|
|
46
51
|
stderr.write(`mcp-for-i control plane already running at ${url}\n`);
|
|
47
|
-
|
|
52
|
+
return;
|
|
48
53
|
}
|
|
49
54
|
throw err;
|
|
50
55
|
}
|
|
56
|
+
stderr.write(`mcp-for-i control plane running at ${getControlPlaneUrl()}\n`);
|
|
57
|
+
const shutdown = () => {
|
|
58
|
+
server.close().catch(() => undefined);
|
|
59
|
+
};
|
|
60
|
+
process.once("SIGINT", shutdown);
|
|
61
|
+
process.once("SIGTERM", shutdown);
|
|
62
|
+
await server.closed;
|
|
63
|
+
if (await server.shutdownRequested) {
|
|
64
|
+
stderr.write("mcp-for-i control plane stopped\n");
|
|
65
|
+
// Background timers (update checks, client probes) must not keep a stopped server's process alive.
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function stop(stderr, scriptPath, nodePath) {
|
|
70
|
+
const url = getControlPlaneUrl();
|
|
71
|
+
const result = await stopControlPlane(scriptPath, nodePath);
|
|
72
|
+
if (!result.wasRunning) {
|
|
73
|
+
stderr.write(`Control plane is not running at ${url}\n`);
|
|
74
|
+
}
|
|
75
|
+
else if (result.stopped) {
|
|
76
|
+
stderr.write(`Stopped control plane at ${url}\n`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
stderr.write(`Control plane at ${url} did not stop: ${result.detail || "it is still answering"}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
if (result.serviceEnabled) {
|
|
83
|
+
stderr.write(`The background service is still enabled (${result.managedBy}), so it starts again at the next login. Run "mcp-for-i control disable" to turn that off.\n`);
|
|
84
|
+
}
|
|
51
85
|
}
|
|
52
86
|
async function printStatus(stderr, scriptPath, nodePath) {
|
|
53
87
|
const url = getControlPlaneUrl();
|
|
@@ -61,12 +95,17 @@ async function printStatus(stderr, scriptPath, nodePath) {
|
|
|
61
95
|
if (status.detail)
|
|
62
96
|
stderr.write(`Detail: ${status.detail}\n`);
|
|
63
97
|
}
|
|
64
|
-
async function openControlUi(stderr, scriptPath, nodePath) {
|
|
98
|
+
async function openControlUi(stderr, scriptPath, nodePath, openBrowser = true) {
|
|
65
99
|
const url = getControlPlaneUrl();
|
|
66
100
|
const result = await ensureControlPlaneRunning(scriptPath, nodePath);
|
|
67
101
|
if (result.started) {
|
|
68
102
|
stderr.write(`Started control plane at ${url}\n`);
|
|
69
103
|
}
|
|
104
|
+
if (!openBrowser) {
|
|
105
|
+
if (!result.started)
|
|
106
|
+
stderr.write(`Control plane is running at ${url}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
70
109
|
try {
|
|
71
110
|
await openUrl(url);
|
|
72
111
|
stderr.write(`Opening ${url}\n`);
|
|
@@ -94,6 +133,8 @@ function printHelp(stderr) {
|
|
|
94
133
|
stderr.write(`mcp-for-i-control commands:\n`);
|
|
95
134
|
stderr.write(` serve Start control plane server in the foreground\n`);
|
|
96
135
|
stderr.write(` open Start control plane in the background if needed, then open UI\n`);
|
|
136
|
+
stderr.write(` stop Stop the running control plane\n`);
|
|
137
|
+
stderr.write(` restart Stop the control plane, then start it again in the background\n`);
|
|
97
138
|
stderr.write(` status Show running and background-service status\n`);
|
|
98
139
|
stderr.write(` enable Enable background service for this platform\n`);
|
|
99
140
|
stderr.write(` disable Disable background service for this platform\n`);
|
|
@@ -1,16 +1,96 @@
|
|
|
1
1
|
import { deletePassword, getPassword, setPassword } from "../security/credentialStore.js";
|
|
2
|
+
import { ControlPlaneError } from "./errors.js";
|
|
3
|
+
import { testConnection } from "./connectionTest.js";
|
|
4
|
+
/** True when host, port or username differ (host compared case-insensitively). */
|
|
5
|
+
export function connectionEndpointChanged(before, after) {
|
|
6
|
+
const host = (value) => String(value ?? "").trim().toLowerCase();
|
|
7
|
+
const port = (value) => Number(value || 22);
|
|
8
|
+
const user = (value) => String(value ?? "").trim().toUpperCase();
|
|
9
|
+
return host(before.host) !== host(after.host)
|
|
10
|
+
|| port(before.port) !== port(after.port)
|
|
11
|
+
|| user(before.username) !== user(after.username);
|
|
12
|
+
}
|
|
2
13
|
export class ConnectionService {
|
|
3
14
|
store;
|
|
4
|
-
|
|
15
|
+
runTest;
|
|
16
|
+
/** Last test result per saved connection, kept in memory only. */
|
|
17
|
+
lastTests = new Map();
|
|
18
|
+
/** Endpoints with a test in progress, so a double click signs on once. */
|
|
19
|
+
running = new Set();
|
|
20
|
+
constructor(store, runTest = testConnection) {
|
|
5
21
|
this.store = store;
|
|
22
|
+
this.runTest = runTest;
|
|
6
23
|
}
|
|
7
24
|
async list() {
|
|
8
|
-
|
|
25
|
+
const list = await this.store.listConnections();
|
|
26
|
+
return list.map(conn => {
|
|
27
|
+
const lastTest = this.lastTests.get(conn.name);
|
|
28
|
+
return lastTest ? { ...conn, lastTest } : conn;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Tests a saved connection with its stored details and password. */
|
|
32
|
+
async testSaved(name) {
|
|
33
|
+
const stored = await this.store.getConnection(name);
|
|
34
|
+
if (!stored)
|
|
35
|
+
throw new ControlPlaneError(404, `Connection ${name} not found`);
|
|
36
|
+
const password = await getPassword(name).catch(() => undefined);
|
|
37
|
+
const result = await this.guardedTest({
|
|
38
|
+
host: stored.host,
|
|
39
|
+
port: stored.port,
|
|
40
|
+
username: stored.username,
|
|
41
|
+
password: password || undefined,
|
|
42
|
+
privateKeyPath: stored.privateKeyPath
|
|
43
|
+
});
|
|
44
|
+
this.lastTests.set(name, result);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Tests details that may not be saved yet (the add and edit form). A saved
|
|
49
|
+
* password is only used for the same host, port and user it was saved for.
|
|
50
|
+
*/
|
|
51
|
+
async testDetails(input) {
|
|
52
|
+
const host = typeof input?.host === "string" ? input.host.trim() : "";
|
|
53
|
+
const username = typeof input?.username === "string" ? input.username.trim() : "";
|
|
54
|
+
if (!host || !username)
|
|
55
|
+
throw new ControlPlaneError(400, "host and username are required");
|
|
56
|
+
const port = Number(input.port || 22);
|
|
57
|
+
let password = typeof input.password === "string" && input.password ? input.password : undefined;
|
|
58
|
+
const name = typeof input.name === "string" ? input.name.trim() : "";
|
|
59
|
+
const stored = name ? await this.store.getConnection(name) : undefined;
|
|
60
|
+
if (!password && stored && !connectionEndpointChanged(stored, { host, port, username })) {
|
|
61
|
+
password = (await getPassword(name).catch(() => undefined)) || undefined;
|
|
62
|
+
}
|
|
63
|
+
const privateKeyPath = typeof input.privateKeyPath === "string" && input.privateKeyPath.trim()
|
|
64
|
+
? input.privateKeyPath.trim()
|
|
65
|
+
: undefined;
|
|
66
|
+
return this.guardedTest({ host, port, username, password, privateKeyPath });
|
|
67
|
+
}
|
|
68
|
+
async guardedTest(target) {
|
|
69
|
+
const key = [target.host.trim().toLowerCase(), Number(target.port || 22), target.username.trim().toUpperCase()].join("|");
|
|
70
|
+
if (this.running.has(key)) {
|
|
71
|
+
throw new ControlPlaneError(409, "A test of this connection is already running");
|
|
72
|
+
}
|
|
73
|
+
this.running.add(key);
|
|
74
|
+
try {
|
|
75
|
+
return await this.runTest(target);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.running.delete(key);
|
|
79
|
+
}
|
|
9
80
|
}
|
|
10
81
|
async get(name) {
|
|
11
82
|
return this.store.getConnection(name);
|
|
12
83
|
}
|
|
13
84
|
async add(input) {
|
|
85
|
+
const name = typeof input?.name === "string" ? input.name.trim() : "";
|
|
86
|
+
if (!name || !input.host || !input.username) {
|
|
87
|
+
throw new ControlPlaneError(400, "name, host and username are required");
|
|
88
|
+
}
|
|
89
|
+
// Adding must never overwrite an existing entry: that would let a caller
|
|
90
|
+
// repoint a connection without the checks in update().
|
|
91
|
+
if (await this.store.getConnection(input.name)) {
|
|
92
|
+
throw new ControlPlaneError(409, `Connection ${input.name} already exists`);
|
|
93
|
+
}
|
|
14
94
|
await this.store.upsertConnection({
|
|
15
95
|
name: input.name,
|
|
16
96
|
host: input.host,
|
|
@@ -28,7 +108,7 @@ export class ConnectionService {
|
|
|
28
108
|
async update(name, input) {
|
|
29
109
|
const existing = await this.store.getConnection(name);
|
|
30
110
|
if (!existing)
|
|
31
|
-
throw new
|
|
111
|
+
throw new ControlPlaneError(404, `Connection ${name} not found`);
|
|
32
112
|
const next = {
|
|
33
113
|
...existing,
|
|
34
114
|
host: input.host ?? existing.host,
|
|
@@ -38,9 +118,28 @@ export class ConnectionService {
|
|
|
38
118
|
settings: input.settings ? { ...(existing.settings || {}), ...input.settings } : existing.settings,
|
|
39
119
|
policy: input.policy ?? existing.policy
|
|
40
120
|
};
|
|
121
|
+
// A saved password must never follow the connection to a different
|
|
122
|
+
// endpoint or user. Changing host, port or username therefore needs the
|
|
123
|
+
// password in the same request.
|
|
124
|
+
const endpointChanged = connectionEndpointChanged(existing, next);
|
|
125
|
+
const newPassword = typeof input.password === "string" && input.password ? input.password : undefined;
|
|
126
|
+
if (endpointChanged && !newPassword && await getPassword(name)) {
|
|
127
|
+
throw new ControlPlaneError(400, "Changing host, port or username requires entering the password again in the same request");
|
|
128
|
+
}
|
|
41
129
|
await this.store.upsertConnection(next);
|
|
42
|
-
|
|
43
|
-
|
|
130
|
+
// An earlier result says nothing about new details or a new password.
|
|
131
|
+
if (endpointChanged || newPassword || next.privateKeyPath !== existing.privateKeyPath) {
|
|
132
|
+
this.lastTests.delete(name);
|
|
133
|
+
}
|
|
134
|
+
if (newPassword && (input.storePassword || endpointChanged)) {
|
|
135
|
+
if (input.storePassword === false) {
|
|
136
|
+
// The caller does not want it stored; drop the old one so it is not
|
|
137
|
+
// sent to the new endpoint.
|
|
138
|
+
await deletePassword(name);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await setPassword(name, newPassword);
|
|
142
|
+
}
|
|
44
143
|
}
|
|
45
144
|
return this.get(name);
|
|
46
145
|
}
|
|
@@ -64,16 +163,22 @@ export class ConnectionService {
|
|
|
64
163
|
await setPassword(newName, password);
|
|
65
164
|
}
|
|
66
165
|
await deletePassword(oldName);
|
|
166
|
+
const lastTest = this.lastTests.get(oldName);
|
|
167
|
+
this.lastTests.delete(oldName);
|
|
168
|
+
if (lastTest)
|
|
169
|
+
this.lastTests.set(newName, lastTest);
|
|
67
170
|
return this.get(newName);
|
|
68
171
|
}
|
|
69
172
|
async delete(name) {
|
|
70
173
|
await this.store.deleteConnection(name);
|
|
71
174
|
await deletePassword(name);
|
|
175
|
+
this.lastTests.delete(name);
|
|
72
176
|
}
|
|
73
177
|
async setConnectionPassword(name, password) {
|
|
74
178
|
const existing = await this.store.getConnection(name);
|
|
75
179
|
if (!existing)
|
|
76
180
|
throw new Error(`Connection ${name} not found`);
|
|
77
181
|
await setPassword(name, password);
|
|
182
|
+
this.lastTests.delete(name);
|
|
78
183
|
}
|
|
79
184
|
}
|