mcp-for-i 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/config/store.js +365 -0
- package/dist/controlplane/autostart.js +93 -0
- package/dist/controlplane/connectionService.js +79 -0
- package/dist/controlplane/index.js +120 -0
- package/dist/controlplane/runtimeService.js +263 -0
- package/dist/controlplane/server.js +180 -0
- package/dist/controlplane/ui.js +741 -0
- package/dist/ibmi/CompileTools.js +91 -0
- package/dist/ibmi/IBMiContent.js +386 -0
- package/dist/ibmi/LocalLanguageActions.js +183 -0
- package/dist/ibmi/Search.js +135 -0
- package/dist/ibmi/Tools.js +188 -0
- package/dist/ibmi/client.js +213 -0
- package/dist/ibmi/sourceDates.js +60 -0
- package/dist/ibmi/types.js +1 -0
- package/dist/ibmi/variables.js +53 -0
- package/dist/index.js +59 -0
- package/dist/mcp/context.js +235 -0
- package/dist/mcp/logger.js +73 -0
- package/dist/mcp/tools.js +1335 -0
- package/dist/mcp/validation.js +74 -0
- package/dist/security/credentialStore.js +42 -0
- package/docs/configuration.md +57 -0
- package/docs/examples.md +73 -0
- package/docs/installation.md +61 -0
- package/docs/overview.md +14 -0
- package/docs/security.md +8 -0
- package/docs/tools.md +105 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# MCP-for-i
|
|
2
|
+
|
|
3
|
+
Local MCP server for IBM i, mirroring core Code for IBM i capabilities. See `docs/` for setup and usage.
|
|
4
|
+
|
|
5
|
+
## Install As CLI Package
|
|
6
|
+
|
|
7
|
+
Install globally:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g mcp-for-i
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run control-plane UI server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mcp-for-i-control serve
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then open:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
http://127.0.0.1:3980
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Background Startup (Windows)
|
|
26
|
+
|
|
27
|
+
Configure autostart at login and launch now:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
mcp-for-i-control setup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Check status:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mcp-for-i-control status
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Disable autostart:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mcp-for-i-control remove
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Control Plane UI
|
|
46
|
+
|
|
47
|
+
Run the local onboarding/control UI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run start:controlplane
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then open `http://127.0.0.1:3980`.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { setPassword } from "../security/credentialStore.js";
|
|
5
|
+
const defaultSettings = {
|
|
6
|
+
readOnlyMode: false,
|
|
7
|
+
tempLibrary: "ILEDITOR",
|
|
8
|
+
tempDir: "/tmp",
|
|
9
|
+
autoClearTempData: true,
|
|
10
|
+
sourceFileCCSID: "*FILE",
|
|
11
|
+
sqlJobCcsid: 1208,
|
|
12
|
+
enableSourceDates: true,
|
|
13
|
+
homeDirectory: ".",
|
|
14
|
+
libraryList: [],
|
|
15
|
+
currentLibrary: "",
|
|
16
|
+
customVariables: [],
|
|
17
|
+
objectFilters: [],
|
|
18
|
+
ifsShortcuts: [],
|
|
19
|
+
debugPort: 8005,
|
|
20
|
+
debugSepPort: 8008
|
|
21
|
+
};
|
|
22
|
+
export class ConfigStore {
|
|
23
|
+
configDir;
|
|
24
|
+
configPath;
|
|
25
|
+
data = null;
|
|
26
|
+
saveQueue = Promise.resolve();
|
|
27
|
+
constructor() {
|
|
28
|
+
this.configDir = getConfigDir();
|
|
29
|
+
this.configPath = path.join(this.configDir, "config.json");
|
|
30
|
+
}
|
|
31
|
+
async load() {
|
|
32
|
+
await ensureDir(this.configDir);
|
|
33
|
+
let loadedFromDisk = true;
|
|
34
|
+
try {
|
|
35
|
+
const raw = await fs.readFile(this.configPath, "utf8");
|
|
36
|
+
this.data = JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
loadedFromDisk = false;
|
|
40
|
+
this.data = { connections: [], settings: defaultSettings, actions: [] };
|
|
41
|
+
}
|
|
42
|
+
const migrated = await migrateConfigFile(this.data);
|
|
43
|
+
this.data = migrated.config;
|
|
44
|
+
if (!loadedFromDisk || migrated.changed) {
|
|
45
|
+
await this.save();
|
|
46
|
+
}
|
|
47
|
+
return this.data;
|
|
48
|
+
}
|
|
49
|
+
async save() {
|
|
50
|
+
if (!this.data)
|
|
51
|
+
await this.load();
|
|
52
|
+
const payload = JSON.stringify(this.data, null, 2);
|
|
53
|
+
await this.enqueueSave(async () => {
|
|
54
|
+
const tempPath = `${this.configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
55
|
+
await writeFileWithRetry(tempPath, payload);
|
|
56
|
+
try {
|
|
57
|
+
await fs.rename(tempPath, this.configPath);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
await writeFileWithRetry(this.configPath, payload);
|
|
61
|
+
await fs.rm(tempPath, { force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async listConnections() {
|
|
66
|
+
const cfg = await this.load();
|
|
67
|
+
return cfg.connections;
|
|
68
|
+
}
|
|
69
|
+
async getConnection(name) {
|
|
70
|
+
const cfg = await this.load();
|
|
71
|
+
return cfg.connections.find(c => c.name === name);
|
|
72
|
+
}
|
|
73
|
+
async upsertConnection(connection) {
|
|
74
|
+
const cfg = await this.load();
|
|
75
|
+
const sanitized = await sanitizeConnection(connection);
|
|
76
|
+
const idx = cfg.connections.findIndex(c => c.name === sanitized.name);
|
|
77
|
+
if (idx >= 0) {
|
|
78
|
+
cfg.connections[idx] = { ...cfg.connections[idx], ...sanitized };
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
cfg.connections.push(sanitized);
|
|
82
|
+
}
|
|
83
|
+
await this.save();
|
|
84
|
+
}
|
|
85
|
+
async deleteConnection(name) {
|
|
86
|
+
const cfg = await this.load();
|
|
87
|
+
cfg.connections = cfg.connections.filter(c => c.name !== name);
|
|
88
|
+
await this.save();
|
|
89
|
+
}
|
|
90
|
+
async getSettings() {
|
|
91
|
+
const cfg = await this.load();
|
|
92
|
+
return cfg.settings;
|
|
93
|
+
}
|
|
94
|
+
async updateSettings(update) {
|
|
95
|
+
const cfg = await this.load();
|
|
96
|
+
cfg.settings = { ...cfg.settings, ...update };
|
|
97
|
+
await this.save();
|
|
98
|
+
}
|
|
99
|
+
async listActions() {
|
|
100
|
+
const cfg = await this.load();
|
|
101
|
+
return cfg.actions || [];
|
|
102
|
+
}
|
|
103
|
+
async upsertAction(action) {
|
|
104
|
+
const cfg = await this.load();
|
|
105
|
+
const idx = cfg.actions.findIndex(a => a.name === action.name);
|
|
106
|
+
if (idx >= 0)
|
|
107
|
+
cfg.actions[idx] = action;
|
|
108
|
+
else
|
|
109
|
+
cfg.actions.push(action);
|
|
110
|
+
await this.save();
|
|
111
|
+
}
|
|
112
|
+
async deleteAction(name) {
|
|
113
|
+
const cfg = await this.load();
|
|
114
|
+
cfg.actions = cfg.actions.filter(a => a.name !== name);
|
|
115
|
+
await this.save();
|
|
116
|
+
}
|
|
117
|
+
async listProfiles(connectionName) {
|
|
118
|
+
const conn = await this.getConnection(connectionName);
|
|
119
|
+
return conn?.profiles || [];
|
|
120
|
+
}
|
|
121
|
+
async saveProfile(connectionName, profile) {
|
|
122
|
+
const conn = await this.getConnection(connectionName);
|
|
123
|
+
if (!conn)
|
|
124
|
+
throw new Error(`Connection ${connectionName} not found`);
|
|
125
|
+
const profiles = conn.profiles || [];
|
|
126
|
+
const idx = profiles.findIndex(p => p.name === profile.name);
|
|
127
|
+
if (idx >= 0)
|
|
128
|
+
profiles[idx] = profile;
|
|
129
|
+
else
|
|
130
|
+
profiles.push(profile);
|
|
131
|
+
conn.profiles = profiles;
|
|
132
|
+
await this.upsertConnection(conn);
|
|
133
|
+
}
|
|
134
|
+
async deleteProfile(connectionName, profileName) {
|
|
135
|
+
const conn = await this.getConnection(connectionName);
|
|
136
|
+
if (!conn)
|
|
137
|
+
throw new Error(`Connection ${connectionName} not found`);
|
|
138
|
+
conn.profiles = (conn.profiles || []).filter(p => p.name !== profileName);
|
|
139
|
+
if (conn.currentProfile === profileName) {
|
|
140
|
+
conn.currentProfile = undefined;
|
|
141
|
+
}
|
|
142
|
+
await this.upsertConnection(conn);
|
|
143
|
+
}
|
|
144
|
+
async setCurrentProfile(connectionName, profileName) {
|
|
145
|
+
const conn = await this.getConnection(connectionName);
|
|
146
|
+
if (!conn)
|
|
147
|
+
throw new Error(`Connection ${connectionName} not found`);
|
|
148
|
+
conn.currentProfile = profileName;
|
|
149
|
+
await this.upsertConnection(conn);
|
|
150
|
+
}
|
|
151
|
+
enqueueSave(operation) {
|
|
152
|
+
this.saveQueue = this.saveQueue.then(operation, operation);
|
|
153
|
+
return this.saveQueue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function getConfigDir() {
|
|
157
|
+
const platform = process.platform;
|
|
158
|
+
if (platform === "win32") {
|
|
159
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "mcp-for-i");
|
|
160
|
+
}
|
|
161
|
+
if (platform === "darwin") {
|
|
162
|
+
return path.join(os.homedir(), "Library", "Application Support", "mcp-for-i");
|
|
163
|
+
}
|
|
164
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "mcp-for-i");
|
|
165
|
+
}
|
|
166
|
+
async function migrateConfigFile(input) {
|
|
167
|
+
let changed = false;
|
|
168
|
+
const root = isPlainObject(input) ? input : {};
|
|
169
|
+
if (!isPlainObject(input))
|
|
170
|
+
changed = true;
|
|
171
|
+
const settings = normalizeSettings(root.settings);
|
|
172
|
+
changed = changed || settings.changed;
|
|
173
|
+
const actions = normalizeActions(root.actions);
|
|
174
|
+
changed = changed || actions.changed;
|
|
175
|
+
const normalizedConnections = await normalizeConnections(root.connections);
|
|
176
|
+
changed = changed || normalizedConnections.changed;
|
|
177
|
+
return {
|
|
178
|
+
changed,
|
|
179
|
+
config: {
|
|
180
|
+
connections: normalizedConnections.connections,
|
|
181
|
+
settings: settings.value,
|
|
182
|
+
actions: actions.value
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function normalizeConnections(value) {
|
|
187
|
+
if (!Array.isArray(value)) {
|
|
188
|
+
return { connections: [], changed: true };
|
|
189
|
+
}
|
|
190
|
+
let changed = false;
|
|
191
|
+
const seenNames = new Set();
|
|
192
|
+
const connections = [];
|
|
193
|
+
for (const [index, entry] of value.entries()) {
|
|
194
|
+
if (!isPlainObject(entry)) {
|
|
195
|
+
changed = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
let name = toNonEmptyString(entry.name) || toNonEmptyString(entry.id);
|
|
199
|
+
if (!name) {
|
|
200
|
+
const user = toNonEmptyString(entry.username);
|
|
201
|
+
const host = toNonEmptyString(entry.host);
|
|
202
|
+
if (user && host) {
|
|
203
|
+
name = `${user}@${host}`;
|
|
204
|
+
}
|
|
205
|
+
else if (host) {
|
|
206
|
+
name = host;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
name = `connection-${index + 1}`;
|
|
210
|
+
}
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
const uniqueName = uniqueConnectionName(name, seenNames);
|
|
214
|
+
if (uniqueName !== name)
|
|
215
|
+
changed = true;
|
|
216
|
+
seenNames.add(uniqueName);
|
|
217
|
+
const password = toNonEmptyString(entry.password);
|
|
218
|
+
if (password) {
|
|
219
|
+
await setPassword(uniqueName, password);
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
const host = toNonEmptyString(entry.host) || "";
|
|
223
|
+
const username = toNonEmptyString(entry.username) || "";
|
|
224
|
+
const port = normalizePort(entry.port);
|
|
225
|
+
const privateKeyPath = toNonEmptyString(entry.privateKeyPath);
|
|
226
|
+
const settings = isPlainObject(entry.settings) ? entry.settings : undefined;
|
|
227
|
+
const profiles = Array.isArray(entry.profiles) ? entry.profiles : undefined;
|
|
228
|
+
const currentProfile = toNonEmptyString(entry.currentProfile);
|
|
229
|
+
const policy = normalizePolicy(entry.policy);
|
|
230
|
+
const keepaliveInterval = normalizeOptionalNumber(entry.keepaliveInterval);
|
|
231
|
+
const readyTimeout = normalizeOptionalNumber(entry.readyTimeout);
|
|
232
|
+
const sshDebug = typeof entry.sshDebug === "boolean" ? entry.sshDebug : undefined;
|
|
233
|
+
const normalized = {
|
|
234
|
+
name: uniqueName,
|
|
235
|
+
host,
|
|
236
|
+
port,
|
|
237
|
+
username
|
|
238
|
+
};
|
|
239
|
+
if (privateKeyPath)
|
|
240
|
+
normalized.privateKeyPath = privateKeyPath;
|
|
241
|
+
if (settings)
|
|
242
|
+
normalized.settings = settings;
|
|
243
|
+
if (profiles)
|
|
244
|
+
normalized.profiles = profiles;
|
|
245
|
+
if (currentProfile)
|
|
246
|
+
normalized.currentProfile = currentProfile;
|
|
247
|
+
if (policy)
|
|
248
|
+
normalized.policy = policy;
|
|
249
|
+
if (keepaliveInterval !== undefined)
|
|
250
|
+
normalized.keepaliveInterval = keepaliveInterval;
|
|
251
|
+
if (readyTimeout !== undefined)
|
|
252
|
+
normalized.readyTimeout = readyTimeout;
|
|
253
|
+
if (sshDebug !== undefined)
|
|
254
|
+
normalized.sshDebug = sshDebug;
|
|
255
|
+
if (JSON.stringify(normalized) !== JSON.stringify(entry)) {
|
|
256
|
+
changed = true;
|
|
257
|
+
}
|
|
258
|
+
connections.push(normalized);
|
|
259
|
+
}
|
|
260
|
+
return { connections, changed };
|
|
261
|
+
}
|
|
262
|
+
function normalizeSettings(value) {
|
|
263
|
+
if (!isPlainObject(value)) {
|
|
264
|
+
return { value: { ...defaultSettings }, changed: true };
|
|
265
|
+
}
|
|
266
|
+
const merged = { ...defaultSettings, ...value };
|
|
267
|
+
const changed = JSON.stringify(merged) !== JSON.stringify(value);
|
|
268
|
+
return { value: merged, changed };
|
|
269
|
+
}
|
|
270
|
+
function normalizeActions(value) {
|
|
271
|
+
if (!Array.isArray(value)) {
|
|
272
|
+
return { value: [], changed: true };
|
|
273
|
+
}
|
|
274
|
+
const filtered = value.filter(isPlainObject);
|
|
275
|
+
const changed = filtered.length !== value.length;
|
|
276
|
+
return { value: filtered, changed };
|
|
277
|
+
}
|
|
278
|
+
function normalizePolicy(value) {
|
|
279
|
+
if (!isPlainObject(value))
|
|
280
|
+
return undefined;
|
|
281
|
+
const profile = value.profile;
|
|
282
|
+
const normalizedProfile = profile === "read-only" || profile === "power-user" || profile === "guarded" ? profile : "guarded";
|
|
283
|
+
const policy = { ...value, profile: normalizedProfile };
|
|
284
|
+
if (!Array.isArray(policy.requireApprovalFor))
|
|
285
|
+
delete policy.requireApprovalFor;
|
|
286
|
+
if (!Array.isArray(policy.allowCommands))
|
|
287
|
+
delete policy.allowCommands;
|
|
288
|
+
if (!Array.isArray(policy.denyCommands))
|
|
289
|
+
delete policy.denyCommands;
|
|
290
|
+
if (!Array.isArray(policy.allowLibraries))
|
|
291
|
+
delete policy.allowLibraries;
|
|
292
|
+
if (!Array.isArray(policy.denyLibraries))
|
|
293
|
+
delete policy.denyLibraries;
|
|
294
|
+
if (!Array.isArray(policy.allowPaths))
|
|
295
|
+
delete policy.allowPaths;
|
|
296
|
+
if (!Array.isArray(policy.denyPaths))
|
|
297
|
+
delete policy.denyPaths;
|
|
298
|
+
return policy;
|
|
299
|
+
}
|
|
300
|
+
async function sanitizeConnection(connection) {
|
|
301
|
+
const raw = connection;
|
|
302
|
+
const password = toNonEmptyString(raw.password);
|
|
303
|
+
if (password) {
|
|
304
|
+
await setPassword(connection.name, password);
|
|
305
|
+
}
|
|
306
|
+
const { password: _password, storePassword: _storePassword, ...rest } = raw;
|
|
307
|
+
const normalizedPort = normalizePort(rest.port);
|
|
308
|
+
return {
|
|
309
|
+
...rest,
|
|
310
|
+
port: normalizedPort
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function uniqueConnectionName(baseName, seen) {
|
|
314
|
+
if (!seen.has(baseName))
|
|
315
|
+
return baseName;
|
|
316
|
+
let suffix = 2;
|
|
317
|
+
let candidate = `${baseName}-${suffix}`;
|
|
318
|
+
while (seen.has(candidate)) {
|
|
319
|
+
suffix += 1;
|
|
320
|
+
candidate = `${baseName}-${suffix}`;
|
|
321
|
+
}
|
|
322
|
+
return candidate;
|
|
323
|
+
}
|
|
324
|
+
function normalizePort(value) {
|
|
325
|
+
const port = Number(value);
|
|
326
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
327
|
+
return 22;
|
|
328
|
+
}
|
|
329
|
+
return Math.floor(port);
|
|
330
|
+
}
|
|
331
|
+
function normalizeOptionalNumber(value) {
|
|
332
|
+
const n = Number(value);
|
|
333
|
+
if (!Number.isFinite(n))
|
|
334
|
+
return undefined;
|
|
335
|
+
return Math.floor(n);
|
|
336
|
+
}
|
|
337
|
+
function toNonEmptyString(value) {
|
|
338
|
+
if (typeof value !== "string")
|
|
339
|
+
return undefined;
|
|
340
|
+
const trimmed = value.trim();
|
|
341
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
342
|
+
}
|
|
343
|
+
function isPlainObject(value) {
|
|
344
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
345
|
+
}
|
|
346
|
+
async function ensureDir(dir) {
|
|
347
|
+
await fs.mkdir(dir, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
async function writeFileWithRetry(filePath, content, retries = 5, delayMs = 40) {
|
|
350
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
351
|
+
try {
|
|
352
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
const transient = err?.code === "EBUSY" || err?.code === "EPERM";
|
|
357
|
+
if (!transient || attempt === retries)
|
|
358
|
+
throw err;
|
|
359
|
+
await wait(delayMs * (attempt + 1));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function wait(ms) {
|
|
364
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
365
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export const AUTOSTART_TASK_NAME = "mcp-for-i-controlplane";
|
|
3
|
+
export function getControlPlaneUrl() {
|
|
4
|
+
const host = process.env.MCP_FOR_I_CONTROL_HOST || "127.0.0.1";
|
|
5
|
+
const portRaw = Number(process.env.MCP_FOR_I_CONTROL_PORT || 3980);
|
|
6
|
+
const port = Number.isFinite(portRaw) && portRaw > 0 && portRaw <= 65535 ? Math.floor(portRaw) : 3980;
|
|
7
|
+
return `http://${host}:${port}`;
|
|
8
|
+
}
|
|
9
|
+
export async function isControlPlaneRunning(baseUrl = getControlPlaneUrl()) {
|
|
10
|
+
try {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
13
|
+
const res = await fetch(`${baseUrl}/api/health`, { signal: controller.signal });
|
|
14
|
+
clearTimeout(timeout);
|
|
15
|
+
return res.ok;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function configureWindowsAutostart(scriptPath, nodePath = process.execPath) {
|
|
22
|
+
assertWindowsPlatform();
|
|
23
|
+
const taskCmd = `"${nodePath}" "${scriptPath}" serve`;
|
|
24
|
+
await runCommand("schtasks", [
|
|
25
|
+
"/Create",
|
|
26
|
+
"/F",
|
|
27
|
+
"/SC",
|
|
28
|
+
"ONLOGON",
|
|
29
|
+
"/RL",
|
|
30
|
+
"LIMITED",
|
|
31
|
+
"/TN",
|
|
32
|
+
AUTOSTART_TASK_NAME,
|
|
33
|
+
"/TR",
|
|
34
|
+
taskCmd
|
|
35
|
+
]);
|
|
36
|
+
await runCommand("schtasks", ["/Run", "/TN", AUTOSTART_TASK_NAME]);
|
|
37
|
+
}
|
|
38
|
+
export async function removeWindowsAutostart() {
|
|
39
|
+
assertWindowsPlatform();
|
|
40
|
+
await runCommand("schtasks", ["/Delete", "/TN", AUTOSTART_TASK_NAME, "/F"]);
|
|
41
|
+
}
|
|
42
|
+
export async function getWindowsAutostartStatus() {
|
|
43
|
+
assertWindowsPlatform();
|
|
44
|
+
try {
|
|
45
|
+
const output = await runCommandCapture("schtasks", ["/Query", "/TN", AUTOSTART_TASK_NAME, "/FO", "LIST"]);
|
|
46
|
+
const lower = output.toLowerCase();
|
|
47
|
+
const hasTask = lower.includes("taskname:");
|
|
48
|
+
const stateLine = output
|
|
49
|
+
.split(/\r?\n/)
|
|
50
|
+
.find(line => line.trim().toLowerCase().startsWith("status:"));
|
|
51
|
+
return {
|
|
52
|
+
installed: hasTask,
|
|
53
|
+
state: stateLine ? stateLine.split(":").slice(1).join(":").trim() : undefined,
|
|
54
|
+
raw: output
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {
|
|
59
|
+
installed: false,
|
|
60
|
+
state: undefined,
|
|
61
|
+
raw: ""
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function runCommand(command, args) {
|
|
66
|
+
await runCommandCapture(command, args);
|
|
67
|
+
}
|
|
68
|
+
export async function runCommandCapture(command, args) {
|
|
69
|
+
return await new Promise((resolve, reject) => {
|
|
70
|
+
const child = spawn(command, args, { shell: process.platform === "win32" });
|
|
71
|
+
let output = "";
|
|
72
|
+
child.stdout.on("data", chunk => {
|
|
73
|
+
output += String(chunk);
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on("data", chunk => {
|
|
76
|
+
output += String(chunk);
|
|
77
|
+
});
|
|
78
|
+
child.on("error", reject);
|
|
79
|
+
child.on("exit", code => {
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
resolve(output.trim());
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
reject(new Error(output.trim() || `Command failed (${code}): ${command} ${args.join(" ")}`));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function assertWindowsPlatform() {
|
|
90
|
+
if (process.platform !== "win32") {
|
|
91
|
+
throw new Error("This autostart operation is currently supported on Windows only.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { deletePassword, getPassword, setPassword } from "../security/credentialStore.js";
|
|
2
|
+
export class ConnectionService {
|
|
3
|
+
store;
|
|
4
|
+
constructor(store) {
|
|
5
|
+
this.store = store;
|
|
6
|
+
}
|
|
7
|
+
async list() {
|
|
8
|
+
return this.store.listConnections();
|
|
9
|
+
}
|
|
10
|
+
async get(name) {
|
|
11
|
+
return this.store.getConnection(name);
|
|
12
|
+
}
|
|
13
|
+
async add(input) {
|
|
14
|
+
await this.store.upsertConnection({
|
|
15
|
+
name: input.name,
|
|
16
|
+
host: input.host,
|
|
17
|
+
port: input.port || 22,
|
|
18
|
+
username: input.username,
|
|
19
|
+
privateKeyPath: input.privateKeyPath,
|
|
20
|
+
settings: input.settings,
|
|
21
|
+
policy: input.policy
|
|
22
|
+
});
|
|
23
|
+
if (input.storePassword && input.password) {
|
|
24
|
+
await setPassword(input.name, input.password);
|
|
25
|
+
}
|
|
26
|
+
return this.get(input.name);
|
|
27
|
+
}
|
|
28
|
+
async update(name, input) {
|
|
29
|
+
const existing = await this.store.getConnection(name);
|
|
30
|
+
if (!existing)
|
|
31
|
+
throw new Error(`Connection ${name} not found`);
|
|
32
|
+
const next = {
|
|
33
|
+
...existing,
|
|
34
|
+
host: input.host ?? existing.host,
|
|
35
|
+
port: input.port ?? existing.port,
|
|
36
|
+
username: input.username ?? existing.username,
|
|
37
|
+
privateKeyPath: input.privateKeyPath ?? existing.privateKeyPath,
|
|
38
|
+
settings: input.settings ?? existing.settings,
|
|
39
|
+
policy: input.policy ?? existing.policy
|
|
40
|
+
};
|
|
41
|
+
await this.store.upsertConnection(next);
|
|
42
|
+
if (input.storePassword && input.password) {
|
|
43
|
+
await setPassword(name, input.password);
|
|
44
|
+
}
|
|
45
|
+
return this.get(name);
|
|
46
|
+
}
|
|
47
|
+
async rename(oldName, newName) {
|
|
48
|
+
if (oldName === newName) {
|
|
49
|
+
return this.get(oldName);
|
|
50
|
+
}
|
|
51
|
+
const existing = await this.store.getConnection(oldName);
|
|
52
|
+
if (!existing)
|
|
53
|
+
throw new Error(`Connection ${oldName} not found`);
|
|
54
|
+
if (await this.store.getConnection(newName)) {
|
|
55
|
+
throw new Error(`Connection ${newName} already exists`);
|
|
56
|
+
}
|
|
57
|
+
await this.store.upsertConnection({
|
|
58
|
+
...existing,
|
|
59
|
+
name: newName
|
|
60
|
+
});
|
|
61
|
+
await this.store.deleteConnection(oldName);
|
|
62
|
+
const password = await getPassword(oldName);
|
|
63
|
+
if (password) {
|
|
64
|
+
await setPassword(newName, password);
|
|
65
|
+
}
|
|
66
|
+
await deletePassword(oldName);
|
|
67
|
+
return this.get(newName);
|
|
68
|
+
}
|
|
69
|
+
async delete(name) {
|
|
70
|
+
await this.store.deleteConnection(name);
|
|
71
|
+
await deletePassword(name);
|
|
72
|
+
}
|
|
73
|
+
async setConnectionPassword(name, password) {
|
|
74
|
+
const existing = await this.store.getConnection(name);
|
|
75
|
+
if (!existing)
|
|
76
|
+
throw new Error(`Connection ${name} not found`);
|
|
77
|
+
await setPassword(name, password);
|
|
78
|
+
}
|
|
79
|
+
}
|