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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Tools } from "./Tools.js";
|
|
2
|
+
export var Search;
|
|
3
|
+
(function (Search) {
|
|
4
|
+
function parseHitPath(hit) {
|
|
5
|
+
const parts = hit.path.split('/');
|
|
6
|
+
if (parts.length === 4) {
|
|
7
|
+
parts.shift();
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
library: parts[0],
|
|
11
|
+
file: parts[1],
|
|
12
|
+
name: parts[2],
|
|
13
|
+
extension: ''
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function searchMembers(connection, library, sourceFile, searchTerm, members, readOnly) {
|
|
17
|
+
let detailedMembers;
|
|
18
|
+
let memberFilter;
|
|
19
|
+
const pfgrep = connection.remoteFeatures.pfgrep;
|
|
20
|
+
if (typeof members === `string`) {
|
|
21
|
+
if (!isSafeMemberToken(members)) {
|
|
22
|
+
throw new Error(`Invalid members filter: ${members}`);
|
|
23
|
+
}
|
|
24
|
+
memberFilter = `${members}.MBR`;
|
|
25
|
+
}
|
|
26
|
+
else if (Array.isArray(members)) {
|
|
27
|
+
if (members.length > 1000) {
|
|
28
|
+
detailedMembers = members;
|
|
29
|
+
memberFilter = "*.MBR";
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
members.forEach(member => {
|
|
33
|
+
if (!isSafeMemberToken(member.name)) {
|
|
34
|
+
throw new Error(`Invalid member name in filter: ${member.name}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
memberFilter = members.map(member => `${member.name}.MBR`).join(` `);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const command = pfgrep
|
|
41
|
+
? `${pfgrep} -inHr -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`
|
|
42
|
+
: `/usr/bin/grep -inHR -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`;
|
|
43
|
+
const result = await connection.sendCommand({
|
|
44
|
+
command,
|
|
45
|
+
directory: `/QSYS.LIB/${library}.LIB/${sourceFile}.FILE`
|
|
46
|
+
});
|
|
47
|
+
if (!result.stderr) {
|
|
48
|
+
let hits = parseGrepOutput(result.stdout || '', readOnly);
|
|
49
|
+
if (detailedMembers) {
|
|
50
|
+
hits = hits.filter(hit => {
|
|
51
|
+
const hitMember = parseHitPath(hit);
|
|
52
|
+
return detailedMembers.some(member => member.name === hitMember.name && member.library === hitMember.library && member.file === hitMember.file);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { term: searchTerm, hits };
|
|
56
|
+
}
|
|
57
|
+
throw new Error(result.stderr || "Search failed");
|
|
58
|
+
}
|
|
59
|
+
Search.searchMembers = searchMembers;
|
|
60
|
+
async function searchIFS(connection, path, searchTerm) {
|
|
61
|
+
const grep = connection.remoteFeatures.grep;
|
|
62
|
+
if (!grep)
|
|
63
|
+
throw new Error("Grep must be installed on the remote system.");
|
|
64
|
+
const grepRes = await connection.sendCommand({
|
|
65
|
+
command: `${grep} -inr -F -f - ${Tools.escapePath(path)}`,
|
|
66
|
+
stdin: searchTerm
|
|
67
|
+
});
|
|
68
|
+
if (grepRes.code === 0) {
|
|
69
|
+
return { term: searchTerm, hits: parseGrepOutput(grepRes.stdout) };
|
|
70
|
+
}
|
|
71
|
+
// grep returns exit code 1 when no matches are found
|
|
72
|
+
if (grepRes.code === 1 && !grepRes.stderr) {
|
|
73
|
+
return { term: searchTerm, hits: [] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
Search.searchIFS = searchIFS;
|
|
77
|
+
async function findIFS(connection, path, findTerm) {
|
|
78
|
+
const find = connection.remoteFeatures.find;
|
|
79
|
+
if (!find)
|
|
80
|
+
throw new Error("Find must be installed on the remote system.");
|
|
81
|
+
const findRes = await connection.sendCommand({
|
|
82
|
+
command: `${find} ${Tools.escapePath(path)} -type f -iname ${Tools.shellQuote(`*${findTerm}*`)} -print`
|
|
83
|
+
});
|
|
84
|
+
if (findRes.code === 0) {
|
|
85
|
+
if (findRes.stdout) {
|
|
86
|
+
return { term: findTerm, hits: parseFindOutput(findRes.stdout) };
|
|
87
|
+
}
|
|
88
|
+
return { term: findTerm, hits: [] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
Search.findIFS = findIFS;
|
|
92
|
+
function parseFindOutput(output, readonly, pathTransformer) {
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const line of output.split('\n')) {
|
|
95
|
+
const path = pathTransformer?.(line) || line;
|
|
96
|
+
results.push(results.find(r => r.path === path) || { path, readonly, lines: [] });
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
function parseGrepOutput(output, readonly, pathTransformer) {
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const line of output.split('\n')) {
|
|
103
|
+
if (line && !line.startsWith(`Binary`)) {
|
|
104
|
+
const parts = line.split(`:`);
|
|
105
|
+
const path = pathTransformer?.(parts[0]) || parts[0];
|
|
106
|
+
let result = results.find(r => r.path === path);
|
|
107
|
+
if (!result) {
|
|
108
|
+
result = { path, lines: [], readonly };
|
|
109
|
+
results.push(result);
|
|
110
|
+
}
|
|
111
|
+
const contentIndex = nthIndex(line, `:`, 2);
|
|
112
|
+
if (contentIndex >= 0) {
|
|
113
|
+
const curContent = line.substring(contentIndex + 1);
|
|
114
|
+
result.lines.push({ number: Number(parts[1]), content: curContent });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
function sanitizeSearchTerm(searchTerm) {
|
|
121
|
+
return searchTerm.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`);
|
|
122
|
+
}
|
|
123
|
+
function isSafeMemberToken(name) {
|
|
124
|
+
return /^[A-Za-z0-9_*#]+$/.test(name);
|
|
125
|
+
}
|
|
126
|
+
function nthIndex(aString, pattern, n) {
|
|
127
|
+
let index = -1;
|
|
128
|
+
while (n-- && index++ < aString.length) {
|
|
129
|
+
index = aString.indexOf(pattern, index);
|
|
130
|
+
if (index < 0)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return index;
|
|
134
|
+
}
|
|
135
|
+
})(Search || (Search = {}));
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
export var Tools;
|
|
3
|
+
(function (Tools) {
|
|
4
|
+
class SqlError extends Error {
|
|
5
|
+
sqlstate = "0";
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
Tools.SqlError = SqlError;
|
|
11
|
+
function db2Parse(output, input) {
|
|
12
|
+
let gotHeaders = false;
|
|
13
|
+
let figuredLengths = false;
|
|
14
|
+
let iiErrorMessage = false;
|
|
15
|
+
const data = output.split(`\n`).filter(line => {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
return trimmed !== `DB2>` &&
|
|
18
|
+
!trimmed.startsWith(`DB20`) &&
|
|
19
|
+
!/COMMAND .+ COMPLETED WITH EXIT STATUS \d+/.test(trimmed) &&
|
|
20
|
+
trimmed !== `?>`;
|
|
21
|
+
});
|
|
22
|
+
if (!data[data.length - 1]) {
|
|
23
|
+
data.pop();
|
|
24
|
+
}
|
|
25
|
+
let headers = [];
|
|
26
|
+
let SQLSTATE = "";
|
|
27
|
+
const rows = [];
|
|
28
|
+
data.forEach((line, index) => {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (trimmed.length === 0 && iiErrorMessage)
|
|
31
|
+
iiErrorMessage = false;
|
|
32
|
+
if (trimmed.length === 0 || index === data.length - 1)
|
|
33
|
+
return;
|
|
34
|
+
if (trimmed === `**** CLI ERROR *****`) {
|
|
35
|
+
iiErrorMessage = true;
|
|
36
|
+
if (data.length > index + 3) {
|
|
37
|
+
SQLSTATE = data[index + 1].trim();
|
|
38
|
+
if (SQLSTATE.includes(`:`)) {
|
|
39
|
+
SQLSTATE = SQLSTATE.split(`:`)[1].trim();
|
|
40
|
+
}
|
|
41
|
+
if (!SQLSTATE.startsWith(`01`)) {
|
|
42
|
+
const errorMessage = data[index + 3] ? data[index + 3].trim() : `Unknown error`;
|
|
43
|
+
let sqlError = new SqlError(`${errorMessage} (${SQLSTATE})`);
|
|
44
|
+
sqlError.sqlstate = SQLSTATE;
|
|
45
|
+
sqlError.cause = input;
|
|
46
|
+
throw sqlError;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (iiErrorMessage)
|
|
52
|
+
return;
|
|
53
|
+
if (!gotHeaders) {
|
|
54
|
+
headers = line.split(` `)
|
|
55
|
+
.filter(header => header.length > 0)
|
|
56
|
+
.map(header => ({ name: header, from: 0, length: 0 }));
|
|
57
|
+
gotHeaders = true;
|
|
58
|
+
}
|
|
59
|
+
else if (!figuredLengths) {
|
|
60
|
+
let base = 0;
|
|
61
|
+
line.split(` `).forEach((header, idx) => {
|
|
62
|
+
headers[idx].from = base;
|
|
63
|
+
headers[idx].length = header.length;
|
|
64
|
+
base += header.length + 1;
|
|
65
|
+
});
|
|
66
|
+
figuredLengths = true;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
let row = {};
|
|
70
|
+
let slideBytesBy = 0;
|
|
71
|
+
headers.forEach(header => {
|
|
72
|
+
const fromPos = header.from - slideBytesBy;
|
|
73
|
+
let strValue = line.substring(fromPos, fromPos + header.length);
|
|
74
|
+
const extendedBytes = strValue
|
|
75
|
+
.split(``)
|
|
76
|
+
.map(c => (Buffer.byteLength(c) < 3 || c.charCodeAt(0) === 65533) ? 0 : 1)
|
|
77
|
+
.reduce((a, b) => a + b, 0);
|
|
78
|
+
slideBytesBy += extendedBytes;
|
|
79
|
+
if (extendedBytes > 0) {
|
|
80
|
+
strValue = strValue.substring(0, strValue.length - extendedBytes);
|
|
81
|
+
}
|
|
82
|
+
let realValue = strValue.trimEnd();
|
|
83
|
+
if (realValue.startsWith(` `)) {
|
|
84
|
+
const asNumber = Number(strValue.trim());
|
|
85
|
+
if (!isNaN(asNumber))
|
|
86
|
+
realValue = asNumber;
|
|
87
|
+
}
|
|
88
|
+
else if (realValue === `-`) {
|
|
89
|
+
realValue = null;
|
|
90
|
+
}
|
|
91
|
+
row[header.name] = realValue;
|
|
92
|
+
});
|
|
93
|
+
rows.push(row);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
Tools.db2Parse = db2Parse;
|
|
99
|
+
function bufferToUx(input) {
|
|
100
|
+
const hexString = Array.from(input)
|
|
101
|
+
.map(char => char.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase())
|
|
102
|
+
.join('');
|
|
103
|
+
return `UX'${hexString}'`;
|
|
104
|
+
}
|
|
105
|
+
Tools.bufferToUx = bufferToUx;
|
|
106
|
+
function makeid(length = 8) {
|
|
107
|
+
let text = `O_`;
|
|
108
|
+
const possible = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`;
|
|
109
|
+
for (let i = 0; i < length; i++)
|
|
110
|
+
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
Tools.makeid = makeid;
|
|
114
|
+
function qualifyPath(library, object, member, iasp, noEscape) {
|
|
115
|
+
[library, object] = Tools.sanitizeObjNamesForPase([library, object]);
|
|
116
|
+
member = member ? Tools.sanitizeObjNamesForPase([member])[0] : undefined;
|
|
117
|
+
iasp = iasp ? Tools.sanitizeObjNamesForPase([iasp])[0] : undefined;
|
|
118
|
+
const libraryPath = library === `QSYS` ? `QSYS.LIB` : `QSYS.LIB/${library}.LIB`;
|
|
119
|
+
const filePath = object ? `${object}.FILE` : '';
|
|
120
|
+
const memberPath = member ? `/${member}.MBR` : '';
|
|
121
|
+
const fullPath = `${libraryPath}/${filePath}${memberPath}`;
|
|
122
|
+
const result = (iasp && iasp.length > 0 ? `/${iasp}` : ``) + `/${noEscape ? fullPath : Tools.escapePath(fullPath)}`;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
Tools.qualifyPath = qualifyPath;
|
|
126
|
+
function escapePath(Path, alreadyQuoted = false) {
|
|
127
|
+
if (alreadyQuoted) {
|
|
128
|
+
return Path.replace(/"|\$|\\/g, matched => `\\`.concat(matched));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return Path.replace(/'|"|\$|&|\\| /g, matched => `\\`.concat(matched));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
Tools.escapePath = escapePath;
|
|
135
|
+
function shellQuote(value) {
|
|
136
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
137
|
+
}
|
|
138
|
+
Tools.shellQuote = shellQuote;
|
|
139
|
+
function sqlEscapeLiteral(value) {
|
|
140
|
+
return String(value).replace(/'/g, "''");
|
|
141
|
+
}
|
|
142
|
+
Tools.sqlEscapeLiteral = sqlEscapeLiteral;
|
|
143
|
+
function sqlString(value) {
|
|
144
|
+
return `'${sqlEscapeLiteral(value)}'`;
|
|
145
|
+
}
|
|
146
|
+
Tools.sqlString = sqlString;
|
|
147
|
+
function isSafeQshToken(value) {
|
|
148
|
+
return /^[A-Za-z0-9_./*-]+$/.test(value);
|
|
149
|
+
}
|
|
150
|
+
Tools.isSafeQshToken = isSafeQshToken;
|
|
151
|
+
function sanitizeObjNamesForPase(libraries) {
|
|
152
|
+
return libraries.map(library => library.startsWith(`#`) ? `"${library}"` : library);
|
|
153
|
+
}
|
|
154
|
+
Tools.sanitizeObjNamesForPase = sanitizeObjNamesForPase;
|
|
155
|
+
function distinct(value, index, array) {
|
|
156
|
+
return array.indexOf(value) === index;
|
|
157
|
+
}
|
|
158
|
+
Tools.distinct = distinct;
|
|
159
|
+
function parseQSysPath(pathStr) {
|
|
160
|
+
const parts = pathStr.split('/').filter(Boolean);
|
|
161
|
+
if (parts.length > 3) {
|
|
162
|
+
return { asp: parts[0], library: parts[1], name: parts[2] };
|
|
163
|
+
}
|
|
164
|
+
return { library: parts[0], name: parts[1] };
|
|
165
|
+
}
|
|
166
|
+
Tools.parseQSysPath = parseQSysPath;
|
|
167
|
+
function fixSQL(statement, removeComments = false) {
|
|
168
|
+
let statements = statement.split("\n").map(line => {
|
|
169
|
+
if (line.startsWith('@')) {
|
|
170
|
+
line = `Call QSYS2.QCMDEXC('${line.substring(1, line.endsWith(";") ? line.length - 1 : undefined).replaceAll("'", "''")}');`;
|
|
171
|
+
}
|
|
172
|
+
return line.replaceAll("--", "\n--");
|
|
173
|
+
}).join(`\n`);
|
|
174
|
+
if (removeComments) {
|
|
175
|
+
statements = statements.split(`\n`).filter(l => !l.trim().startsWith(`--`)).join(`\n`);
|
|
176
|
+
}
|
|
177
|
+
return statements;
|
|
178
|
+
}
|
|
179
|
+
Tools.fixSQL = fixSQL;
|
|
180
|
+
function upperCaseName(value) {
|
|
181
|
+
return value ? value.toUpperCase() : value;
|
|
182
|
+
}
|
|
183
|
+
Tools.upperCaseName = upperCaseName;
|
|
184
|
+
function pathJoinPosix(...parts) {
|
|
185
|
+
return path.posix.join(...parts);
|
|
186
|
+
}
|
|
187
|
+
Tools.pathJoinPosix = pathJoinPosix;
|
|
188
|
+
})(Tools || (Tools = {}));
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { NodeSSH } from "node-ssh";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Tools } from "./Tools.js";
|
|
5
|
+
import { IBMiContent } from "./IBMiContent.js";
|
|
6
|
+
const LOCALE = "LC_ALL=EN_US.UTF-8";
|
|
7
|
+
export class IBMiClient {
|
|
8
|
+
client;
|
|
9
|
+
content;
|
|
10
|
+
remoteFeatures = {};
|
|
11
|
+
defaultUserLibraries = [];
|
|
12
|
+
currentHost = "";
|
|
13
|
+
currentPort = 22;
|
|
14
|
+
currentUser = "";
|
|
15
|
+
currentConnectionName = "";
|
|
16
|
+
config;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.content = new IBMiContent(this);
|
|
20
|
+
}
|
|
21
|
+
getConfig() {
|
|
22
|
+
return this.config;
|
|
23
|
+
}
|
|
24
|
+
setConfig(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
getTempRemote(prefix = "mcpfori") {
|
|
28
|
+
const tempDir = this.config.tempDir || "/tmp";
|
|
29
|
+
const name = `${prefix}_${Tools.makeid(8)}`;
|
|
30
|
+
return path.posix.join(tempDir, name);
|
|
31
|
+
}
|
|
32
|
+
upperCaseName(value) {
|
|
33
|
+
return Tools.upperCaseName(value);
|
|
34
|
+
}
|
|
35
|
+
validQsysName(name) {
|
|
36
|
+
if (!name)
|
|
37
|
+
return false;
|
|
38
|
+
if (name.length > 10)
|
|
39
|
+
return false;
|
|
40
|
+
const upper = this.upperCaseName(name);
|
|
41
|
+
return /^[A-Z][A-Z0-9_.]{0,9}$/.test(upper);
|
|
42
|
+
}
|
|
43
|
+
async connect(connection) {
|
|
44
|
+
this.currentHost = connection.host;
|
|
45
|
+
this.currentPort = connection.port;
|
|
46
|
+
this.currentUser = connection.username;
|
|
47
|
+
this.currentConnectionName = connection.name;
|
|
48
|
+
this.client = new NodeSSH();
|
|
49
|
+
await this.client.connect({
|
|
50
|
+
host: connection.host,
|
|
51
|
+
port: connection.port,
|
|
52
|
+
username: connection.username,
|
|
53
|
+
password: connection.password,
|
|
54
|
+
privateKey: connection.privateKeyPath ? await fs.readFile(connection.privateKeyPath, "utf8") : undefined,
|
|
55
|
+
keepaliveInterval: connection.keepaliveInterval,
|
|
56
|
+
readyTimeout: connection.readyTimeout,
|
|
57
|
+
debug: connection.sshDebug ? (msg) => process.stderr.write(`[ssh] ${msg}\n`) : undefined
|
|
58
|
+
});
|
|
59
|
+
await this.detectRemoteFeatures();
|
|
60
|
+
await this.loadDefaultLibraryList();
|
|
61
|
+
}
|
|
62
|
+
async disconnect() {
|
|
63
|
+
if (this.client) {
|
|
64
|
+
this.client.dispose();
|
|
65
|
+
this.client = undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
static escapeForShell(command) {
|
|
69
|
+
return command.replace(/\$/g, `\\$`);
|
|
70
|
+
}
|
|
71
|
+
async sendQsh(options) {
|
|
72
|
+
const stdin = options.command;
|
|
73
|
+
return this.sendCommand({
|
|
74
|
+
...options,
|
|
75
|
+
command: `${LOCALE} /QOpenSys/usr/bin/qsh`,
|
|
76
|
+
stdin
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async sendCommand(options) {
|
|
80
|
+
if (!this.client)
|
|
81
|
+
throw new Error("Not connected");
|
|
82
|
+
let commands = [];
|
|
83
|
+
if (options.env) {
|
|
84
|
+
commands.push(...Object.entries(options.env).map(([key, value]) => {
|
|
85
|
+
if (!(/^[A-Za-z_]\w*$/).test(key)) {
|
|
86
|
+
throw new Error(`Invalid environment variable name: ${key}`);
|
|
87
|
+
}
|
|
88
|
+
return `export ${key}=${Tools.shellQuote(value || "")}`;
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
commands.push(options.command);
|
|
92
|
+
const command = commands.join(" && ");
|
|
93
|
+
const directory = options.directory || this.config.homeDirectory || ".";
|
|
94
|
+
const result = await this.client.execCommand(command, {
|
|
95
|
+
cwd: directory,
|
|
96
|
+
stdin: options.stdin,
|
|
97
|
+
onStdout: options.onStdout,
|
|
98
|
+
onStderr: options.onStderr
|
|
99
|
+
});
|
|
100
|
+
if (result.code === null)
|
|
101
|
+
result.code = 0;
|
|
102
|
+
if (result.signal === "SIGABRT")
|
|
103
|
+
result.code = 127;
|
|
104
|
+
return {
|
|
105
|
+
code: result.code ?? 0,
|
|
106
|
+
signal: result.signal,
|
|
107
|
+
stdout: result.stdout || "",
|
|
108
|
+
stderr: result.stderr || "",
|
|
109
|
+
command
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async runSQL(statements) {
|
|
113
|
+
const input = Tools.fixSQL(statements, true);
|
|
114
|
+
const ccsid = this.config.sqlJobCcsid ?? 1208;
|
|
115
|
+
const command = `${LOCALE} system "CHGJOB CCSID(${ccsid})" ; ${LOCALE} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`;
|
|
116
|
+
const output = await this.sendCommand({ command, stdin: input, env: { QIBM_PASE_CCSID: String(ccsid) } });
|
|
117
|
+
if (output.stderr && output.stderr.trim()) {
|
|
118
|
+
// QZDFMDB2 tends to emit errors in stdout, but keep stderr for debugging
|
|
119
|
+
}
|
|
120
|
+
if (output.stdout) {
|
|
121
|
+
return Tools.db2Parse(output.stdout, input);
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
async loadDefaultLibraryList() {
|
|
126
|
+
let currentLibrary = "QGPL";
|
|
127
|
+
this.defaultUserLibraries = [];
|
|
128
|
+
const userLibraries = [];
|
|
129
|
+
try {
|
|
130
|
+
const liblResult = await this.sendQsh({ command: `liblist` });
|
|
131
|
+
if (liblResult.code === 0) {
|
|
132
|
+
const lines = (liblResult.stdout || "").split(`\n`);
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (!line.trim())
|
|
135
|
+
continue;
|
|
136
|
+
const lib = line.substring(0, 10).trim();
|
|
137
|
+
const type = line.substring(12).trim();
|
|
138
|
+
switch (type) {
|
|
139
|
+
case `USR`:
|
|
140
|
+
if (lib) {
|
|
141
|
+
userLibraries.push(lib);
|
|
142
|
+
// QSYS cannot be removed from the library list
|
|
143
|
+
if (lib !== `QSYS`)
|
|
144
|
+
this.defaultUserLibraries.push(lib);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
case `CUR`:
|
|
148
|
+
if (lib)
|
|
149
|
+
currentLibrary = lib;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// ignore, leave defaults
|
|
157
|
+
}
|
|
158
|
+
this.defaultUserLibraries = this.defaultUserLibraries.filter(Tools.distinct);
|
|
159
|
+
if (!this.config.currentLibrary) {
|
|
160
|
+
this.config.currentLibrary = currentLibrary;
|
|
161
|
+
}
|
|
162
|
+
if (!this.config.libraryList || this.config.libraryList.length === 0) {
|
|
163
|
+
this.config.libraryList = this.defaultUserLibraries.length > 0 ? [...this.defaultUserLibraries] : userLibraries.filter(Tools.distinct);
|
|
164
|
+
}
|
|
165
|
+
await this.validateConfiguredLibraryList();
|
|
166
|
+
}
|
|
167
|
+
async validateConfiguredLibraryList() {
|
|
168
|
+
const libs = (this.config.libraryList || []).map(l => this.upperCaseName(l));
|
|
169
|
+
if (libs.length === 0)
|
|
170
|
+
return;
|
|
171
|
+
const defaultLibs = Tools.sanitizeObjNamesForPase(this.defaultUserLibraries);
|
|
172
|
+
const sanitized = Tools.sanitizeObjNamesForPase(libs);
|
|
173
|
+
const commands = [
|
|
174
|
+
`liblist -d ${IBMiClient.escapeForShell(defaultLibs.join(` `))}`,
|
|
175
|
+
...sanitized.map(lib => `liblist -a ${IBMiClient.escapeForShell(lib)}`)
|
|
176
|
+
].join(`; `);
|
|
177
|
+
const result = await this.sendQsh({ command: commands });
|
|
178
|
+
const output = [result.stderr, result.stdout].filter(Boolean).join("\n");
|
|
179
|
+
if (output) {
|
|
180
|
+
const bad = libs.filter(lib => {
|
|
181
|
+
const re = new RegExp(`\\b${lib}\\b`, `i`);
|
|
182
|
+
return re.test(output);
|
|
183
|
+
});
|
|
184
|
+
if (bad.length > 0) {
|
|
185
|
+
this.config.libraryList = libs.filter(lib => !bad.includes(lib));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async detectRemoteFeatures() {
|
|
190
|
+
const featurePaths = {
|
|
191
|
+
grep: ["/QOpenSys/pkgs/bin/grep", "/usr/bin/grep"],
|
|
192
|
+
pfgrep: ["/QOpenSys/pkgs/bin/pfgrep"],
|
|
193
|
+
find: ["/QOpenSys/pkgs/bin/find", "/usr/bin/find"],
|
|
194
|
+
tar: ["/QOpenSys/pkgs/bin/tar", "/usr/bin/tar"],
|
|
195
|
+
attr: ["/QOpenSys/pkgs/bin/attr"],
|
|
196
|
+
iconv: ["/usr/bin/iconv", "/QOpenSys/pkgs/bin/iconv"],
|
|
197
|
+
setccsid: ["/QOpenSys/pkgs/bin/setccsid"],
|
|
198
|
+
stat: ["/QOpenSys/pkgs/bin/stat", "/usr/bin/stat"],
|
|
199
|
+
ls: ["/QOpenSys/pkgs/bin/ls", "/usr/bin/ls"]
|
|
200
|
+
};
|
|
201
|
+
for (const [name, paths] of Object.entries(featurePaths)) {
|
|
202
|
+
for (const p of paths) {
|
|
203
|
+
const res = await this.sendCommand({ command: `test -x ${p} && echo ${p}` });
|
|
204
|
+
if (res.stdout.trim()) {
|
|
205
|
+
this.remoteFeatures[name] = p;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// QZDFMDB2.PGM is in QSYS/QZDFMDB2 by default; treat as present
|
|
211
|
+
this.remoteFeatures["QZDFMDB2.PGM"] = "/QSYS.LIB/QZDFMDB2.PGM";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Crypto from "crypto";
|
|
2
|
+
import { diffArrays } from "diff";
|
|
3
|
+
export class SourceDateStore {
|
|
4
|
+
baseDates = new Map();
|
|
5
|
+
baseSource = new Map();
|
|
6
|
+
recordLengths = new Map();
|
|
7
|
+
setBase(key, source, dates, recordLength) {
|
|
8
|
+
this.baseSource.set(key, source);
|
|
9
|
+
this.baseDates.set(key, dates);
|
|
10
|
+
this.recordLengths.set(key, recordLength);
|
|
11
|
+
}
|
|
12
|
+
getRecordLength(key) {
|
|
13
|
+
return this.recordLengths.get(key);
|
|
14
|
+
}
|
|
15
|
+
getBaseSource(key) {
|
|
16
|
+
return this.baseSource.get(key);
|
|
17
|
+
}
|
|
18
|
+
getBaseDates(key) {
|
|
19
|
+
return this.baseDates.get(key);
|
|
20
|
+
}
|
|
21
|
+
calcNewSourceDates(key, newSource) {
|
|
22
|
+
const oldSource = this.baseSource.get(key) || "";
|
|
23
|
+
const oldDates = this.baseDates.get(key) || [];
|
|
24
|
+
const oldLines = oldSource.split("\n");
|
|
25
|
+
const newLines = newSource.split("\n");
|
|
26
|
+
// Use diff to map unchanged lines to old dates
|
|
27
|
+
const diff = diffArrays(oldLines, newLines);
|
|
28
|
+
const newDates = [];
|
|
29
|
+
const today = currentStamp();
|
|
30
|
+
let oldIndex = 0;
|
|
31
|
+
for (const part of diff) {
|
|
32
|
+
if (part.added) {
|
|
33
|
+
for (let i = 0; i < part.value.length; i++) {
|
|
34
|
+
newDates.push(today);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (part.removed) {
|
|
38
|
+
oldIndex += part.value.length;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
for (let i = 0; i < part.value.length; i++) {
|
|
42
|
+
const date = oldDates[oldIndex + i] || today;
|
|
43
|
+
newDates.push(date);
|
|
44
|
+
}
|
|
45
|
+
oldIndex += part.value.length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return newDates;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function makeAliasName(key) {
|
|
52
|
+
return `MCP_${Crypto.createHash("sha1").update(key).digest("hex")}`.toUpperCase();
|
|
53
|
+
}
|
|
54
|
+
export function currentStamp() {
|
|
55
|
+
const today = new Date();
|
|
56
|
+
const mm = today.getMonth() + 1;
|
|
57
|
+
const dd = today.getDate();
|
|
58
|
+
const yy = String(today.getFullYear()).substring(2);
|
|
59
|
+
return [yy, (mm > 9 ? `` : `0`) + mm, (dd > 9 ? `` : `0`) + dd].join(``);
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class Variables extends Map {
|
|
2
|
+
constructor(connection, variables) {
|
|
3
|
+
super(variables instanceof Map ? variables : variables ? Object.entries(variables) : new Map());
|
|
4
|
+
if (connection) {
|
|
5
|
+
const config = connection.getConfig();
|
|
6
|
+
this.set(`&BUILDLIB`, this.get(`&CURLIB`) || config.currentLibrary || "");
|
|
7
|
+
if (!this.has(`&CURLIB`)) {
|
|
8
|
+
this.set(`&CURLIB`, config.currentLibrary || "");
|
|
9
|
+
}
|
|
10
|
+
if (!this.has(`\\*CURLIB`)) {
|
|
11
|
+
this.set(`\\*CURLIB`, config.currentLibrary || "");
|
|
12
|
+
}
|
|
13
|
+
this.set(`&USERNAME`, connection.currentUser)
|
|
14
|
+
.set(`{usrprf}`, connection.currentUser)
|
|
15
|
+
.set(`&HOST`, connection.currentHost)
|
|
16
|
+
.set(`{host}`, connection.currentHost)
|
|
17
|
+
.set(`&HOME`, config.homeDirectory || ".")
|
|
18
|
+
.set(`&WORKDIR`, config.homeDirectory || ".");
|
|
19
|
+
for (const variable of config.customVariables || []) {
|
|
20
|
+
this.set(`&${variable.name.toUpperCase()}`, variable.value || "");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
set(key, value) {
|
|
25
|
+
super.set(key, value);
|
|
26
|
+
this.expandVariables();
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
expandVariables() {
|
|
30
|
+
for (const [key, value] of this.entries()) {
|
|
31
|
+
super.set(key, this.expand(value, [key]));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
expand(input, keysToOmit = []) {
|
|
35
|
+
for (const [key, value] of this.entries()) {
|
|
36
|
+
if (!keysToOmit.includes(key)) {
|
|
37
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
|
+
input = input.replace(new RegExp(escapedKey, `g`), value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return input;
|
|
42
|
+
}
|
|
43
|
+
toPaseVariables() {
|
|
44
|
+
const variables = {};
|
|
45
|
+
for (const [key, value] of this.entries()) {
|
|
46
|
+
const cleanKey = key.startsWith('&') ? key.substring(1) : key;
|
|
47
|
+
if ((/^[a-z_]\w*$/i).test(cleanKey)) {
|
|
48
|
+
variables[cleanKey] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return variables;
|
|
52
|
+
}
|
|
53
|
+
}
|