mcp-for-i 0.1.13 → 0.2.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.
Files changed (67) hide show
  1. package/README.md +17 -0
  2. package/dist/config/store.js +229 -109
  3. package/dist/controlplane/auth.js +128 -0
  4. package/dist/controlplane/clientIntegrationService.js +83 -4
  5. package/dist/controlplane/commands.js +14 -9
  6. package/dist/controlplane/connectionService.js +110 -5
  7. package/dist/controlplane/connectionTest.js +329 -0
  8. package/dist/controlplane/errors.js +12 -0
  9. package/dist/controlplane/runtimeService.js +98 -26
  10. package/dist/controlplane/server.js +122 -11
  11. package/dist/controlplane/serverShared.js +25 -6
  12. package/dist/controlplane/serviceManager.js +113 -100
  13. package/dist/controlplane/ui.js +1742 -920
  14. package/dist/ibmi/CompileTools.js +29 -1
  15. package/dist/ibmi/IBMiContent.js +435 -63
  16. package/dist/ibmi/IbmDocs.js +685 -0
  17. package/dist/ibmi/LocalLanguageActions.js +129 -3
  18. package/dist/ibmi/Search.js +158 -52
  19. package/dist/ibmi/Tools.js +7 -10
  20. package/dist/ibmi/client.js +220 -31
  21. package/dist/ibmi/evfevent.js +256 -0
  22. package/dist/ibmi/knownHosts.js +190 -0
  23. package/dist/ibmi/libraryList.js +93 -0
  24. package/dist/ibmi/sourceInspect.js +815 -0
  25. package/dist/ibmi/sqlJob.js +888 -0
  26. package/dist/ibmi/sqlParse.js +317 -0
  27. package/dist/ibmi/tn5250/certTrust.js +59 -0
  28. package/dist/ibmi/tn5250/client.js +310 -0
  29. package/dist/ibmi/tn5250/datastream.js +109 -0
  30. package/dist/ibmi/tn5250/ebcdic.js +74 -0
  31. package/dist/ibmi/tn5250/index.js +6 -0
  32. package/dist/ibmi/tn5250/screen.js +827 -0
  33. package/dist/ibmi/tn5250/telnet.js +186 -0
  34. package/dist/ibmi/variables.js +18 -7
  35. package/dist/index.js +43 -22
  36. package/dist/mcp/annotations.js +155 -0
  37. package/dist/mcp/audit.js +490 -133
  38. package/dist/mcp/confirm.js +48 -0
  39. package/dist/mcp/context.js +43 -5
  40. package/dist/mcp/deployMethods.js +192 -0
  41. package/dist/mcp/logger.js +5 -10
  42. package/dist/mcp/modules/bulkMembers.js +476 -0
  43. package/dist/mcp/modules/changedLines.js +189 -0
  44. package/dist/mcp/modules/compare.js +198 -0
  45. package/dist/mcp/modules/compile.js +515 -0
  46. package/dist/mcp/modules/ifsOps.js +123 -0
  47. package/dist/mcp/modules/index.js +38 -0
  48. package/dist/mcp/modules/inspect.js +811 -0
  49. package/dist/mcp/modules/libl.js +246 -0
  50. package/dist/mcp/modules/members.js +171 -0
  51. package/dist/mcp/modules/objects.js +339 -0
  52. package/dist/mcp/modules/system.js +266 -0
  53. package/dist/mcp/modules/tn5250.js +515 -0
  54. package/dist/mcp/policy.js +412 -0
  55. package/dist/mcp/tools.js +1700 -698
  56. package/dist/security/credentialStore.js +62 -13
  57. package/dist/security/localPaths.js +138 -0
  58. package/dist/security/redact.js +79 -0
  59. package/dist/util/atomicWrite.js +101 -0
  60. package/dist/util/fileLock.js +206 -0
  61. package/docs/capabilities.md +50 -7
  62. package/docs/configuration.md +189 -60
  63. package/docs/examples.md +109 -73
  64. package/docs/overview.md +15 -15
  65. package/docs/security.md +101 -16
  66. package/docs/tools.md +363 -188
  67. package/package.json +56 -53
@@ -1,6 +1,13 @@
1
+ /**
2
+ * Built-in actions. Streamfile actions compile an IFS source file
3
+ * (&RELATIVEPATH / &FULLPATH); member actions compile a source member
4
+ * (&OPENLIB / &OPENSPF / &OPENMBR). Running an action against the other kind
5
+ * of target is refused.
6
+ */
1
7
  export const LocalLanguageActions = {
2
8
  RPGLE: [
3
9
  {
10
+ type: `streamfile`,
4
11
  name: `Create RPGLE Program`,
5
12
  command: `CRTBNDRPG PGM(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
6
13
  deployFirst: true,
@@ -8,6 +15,7 @@ export const LocalLanguageActions = {
8
15
  extensions: [`RPGLE`]
9
16
  },
10
17
  {
18
+ type: `streamfile`,
11
19
  name: `Create RPGLE Module`,
12
20
  command: `CRTRPGMOD MODULE(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
13
21
  deployFirst: true,
@@ -15,6 +23,7 @@ export const LocalLanguageActions = {
15
23
  extensions: [`RPGLE`]
16
24
  },
17
25
  {
26
+ type: `streamfile`,
18
27
  name: `Create SQLRPGLE Program`,
19
28
  command: `CRTSQLRPGI OBJ(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) CLOSQLCSR(*ENDMOD) CVTCCSID(*JOB) COMPILEOPT('TGTCCSID(*JOB)') RPGPPOPT(*LVL2)`,
20
29
  deployFirst: true,
@@ -22,6 +31,7 @@ export const LocalLanguageActions = {
22
31
  extensions: [`SQLRPGLE`]
23
32
  },
24
33
  {
34
+ type: `streamfile`,
25
35
  name: `Create SQLRPGLE Module`,
26
36
  command: `CRTSQLRPGI OBJ(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OBJTYPE(*MODULE) OPTION(*EVENTF) DBGVIEW(*SOURCE) CLOSQLCSR(*ENDMOD) CVTCCSID(*JOB) COMPILEOPT('TGTCCSID(*JOB)') RPGPPOPT(*LVL2)`,
27
37
  deployFirst: true,
@@ -31,6 +41,7 @@ export const LocalLanguageActions = {
31
41
  ],
32
42
  COBOL: [
33
43
  {
44
+ type: `streamfile`,
34
45
  name: `Create COBOL Program (SQL)`,
35
46
  command: `CRTSQLCBLI OBJ(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) CLOSQLCSR(*ENDMOD) CVTCCSID(*JOB) TOSRCFILE(&CURLIB/QSQLTEMP)`,
36
47
  deployFirst: true,
@@ -38,6 +49,7 @@ export const LocalLanguageActions = {
38
49
  extensions: [`SQLCBL`, `SQLCBBLE`, `SQLCBLLE`, `COB`, `CBLLE`, `CBL`, `CBBLE`]
39
50
  },
40
51
  {
52
+ type: `streamfile`,
41
53
  name: `Create COBOL Module (SQL)`,
42
54
  command: `CRTSQLCBLI OBJ(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OBJTYPE(*MODULE) OPTION(*EVENTF) DBGVIEW(*SOURCE) CLOSQLCSR(*ENDMOD) CVTCCSID(*JOB) TOSRCFILE(&CURLIB/QSQLTEMP)`,
43
55
  deployFirst: true,
@@ -47,6 +59,7 @@ export const LocalLanguageActions = {
47
59
  ],
48
60
  C: [
49
61
  {
62
+ type: `streamfile`,
50
63
  name: `Create C Program`,
51
64
  command: `CRTBNDC PGM(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
52
65
  deployFirst: true,
@@ -54,6 +67,7 @@ export const LocalLanguageActions = {
54
67
  extensions: [`C`]
55
68
  },
56
69
  {
70
+ type: `streamfile`,
57
71
  name: `Create C Module`,
58
72
  command: `CRTCMOD MODULE(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
59
73
  deployFirst: true,
@@ -63,6 +77,7 @@ export const LocalLanguageActions = {
63
77
  ],
64
78
  CPP: [
65
79
  {
80
+ type: `streamfile`,
66
81
  name: `Create CPP Program`,
67
82
  command: `CRTBNDCPP PGM(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
68
83
  deployFirst: true,
@@ -70,6 +85,7 @@ export const LocalLanguageActions = {
70
85
  extensions: [`CPP`]
71
86
  },
72
87
  {
88
+ type: `streamfile`,
73
89
  name: `Create CPP Module`,
74
90
  command: `CRTCPPMOD MODULE(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE) TGTCCSID(*JOB)`,
75
91
  deployFirst: true,
@@ -79,6 +95,7 @@ export const LocalLanguageActions = {
79
95
  ],
80
96
  CLLE: [
81
97
  {
98
+ type: `streamfile`,
82
99
  extensions: [`CLP`, `CLLE`],
83
100
  name: `Create Bound CL Program`,
84
101
  command: `CRTBNDCL PGM(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
@@ -88,6 +105,7 @@ export const LocalLanguageActions = {
88
105
  ],
89
106
  CMD: [
90
107
  {
108
+ type: `streamfile`,
91
109
  extensions: [`cmd`],
92
110
  name: `Create Command`,
93
111
  command: `CRTCMD CMD(&CURLIB/&NAME) PGM(&CURLIB/&NAME) SRCSTMF('&RELATIVEPATH') OPTION(*EVENTF)`,
@@ -97,6 +115,7 @@ export const LocalLanguageActions = {
97
115
  ],
98
116
  SQL: [
99
117
  {
118
+ type: `streamfile`,
100
119
  extensions: [`SQL`, `TABLE`, `VIEW`, `SQLPRC`, `SQLUDF`, `SQLUDT`, `SQLTRG`, `SQLALIAS`, `SQLSEQ`],
101
120
  name: `Run SQL Statements (RUNSQLSTM)`,
102
121
  command: `RUNSQLSTM SRCSTMF('&FULLPATH') COMMIT(*NONE) NAMING(*SQL)`,
@@ -106,11 +125,11 @@ export const LocalLanguageActions = {
106
125
  ],
107
126
  DSPF: [
108
127
  {
128
+ type: "member",
109
129
  name: "Create DSPF",
110
- command: "CRTDSPF FILE(&CURLIB/&NAME) SRCFILE(&SRCFILE) RSTDSP(*NO) OPTION(*EVENTF)",
130
+ command: "CRTDSPF FILE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) RSTDSP(*NO) OPTION(*EVENTF)",
111
131
  environment: "ile",
112
- deployFirst: true,
113
- extensions: ["dspf"]
132
+ extensions: ["DSPF"]
114
133
  }
115
134
  ],
116
135
  "Service Programs": [
@@ -121,6 +140,7 @@ export const LocalLanguageActions = {
121
140
  environment: `ile`
122
141
  },
123
142
  {
143
+ type: `streamfile`,
124
144
  extensions: ["BND", "BINDER"],
125
145
  deployFirst: true,
126
146
  name: "Create Service Program (CRTSRVPGM with source)",
@@ -130,6 +150,7 @@ export const LocalLanguageActions = {
130
150
  ],
131
151
  "GNU Make": [
132
152
  {
153
+ type: `streamfile`,
133
154
  extensions: [`GLOBAL`],
134
155
  name: `Build all with gmake`,
135
156
  command: `/QOpenSys/pkgs/bin/gmake BUILDLIB=&CURLIB ERR=*EVENTF`,
@@ -137,6 +158,7 @@ export const LocalLanguageActions = {
137
158
  deployFirst: true
138
159
  },
139
160
  {
161
+ type: `streamfile`,
140
162
  extensions: [`GLOBAL`],
141
163
  name: `Build current with gmake`,
142
164
  command: `/QOpenSys/pkgs/bin/gmake &BASENAME BUILDLIB=&CURLIB ERR=*EVENTF`,
@@ -146,6 +168,7 @@ export const LocalLanguageActions = {
146
168
  ],
147
169
  "The Object Builder for i (TOBi)": [
148
170
  {
171
+ type: `streamfile`,
149
172
  extensions: [`GLOBAL`],
150
173
  name: `Build all with TOBi`,
151
174
  command: `OPT=*EVENTF BUILDLIB=&CURLIB /QOpenSys/pkgs/bin/makei build`,
@@ -154,6 +177,7 @@ export const LocalLanguageActions = {
154
177
  postDownload: [".logs", ".evfevent"]
155
178
  },
156
179
  {
180
+ type: `streamfile`,
157
181
  extensions: [`GLOBAL`],
158
182
  name: `Build current with TOBi`,
159
183
  command: `OPT=*EVENTF BUILDLIB=&CURLIB /QOpenSys/pkgs/bin/makei compile -f &BASENAME`,
@@ -164,6 +188,7 @@ export const LocalLanguageActions = {
164
188
  ],
165
189
  "Source Orbit": [
166
190
  {
191
+ type: `streamfile`,
167
192
  name: "Build current with Source Orbit",
168
193
  command: "so -bf make -s &RELATIVEPATH && /QOpenSys/pkgs/bin/gmake LIBL='&LIBLS' BIN_LIB=&CURLIB OPT=*EVENTF",
169
194
  environment: "pase",
@@ -172,6 +197,7 @@ export const LocalLanguageActions = {
172
197
  postDownload: [".evfevent/"]
173
198
  },
174
199
  {
200
+ type: `streamfile`,
175
201
  name: "Build entire project with Source Orbit",
176
202
  command: "so -bf make && /QOpenSys/pkgs/bin/gmake LIBL='&LIBLS' BIN_LIB=&CURLIB OPT=*EVENTF",
177
203
  environment: "pase",
@@ -179,5 +205,105 @@ export const LocalLanguageActions = {
179
205
  extensions: ["GLOBAL"],
180
206
  postDownload: [".evfevent/"]
181
207
  }
208
+ ],
209
+ "Source Members": [
210
+ {
211
+ type: `member`,
212
+ name: `Create Bound RPG Program (CRTBNDRPG)`,
213
+ command: `CRTBNDRPG PGM(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
214
+ environment: `ile`,
215
+ extensions: [`RPGLE`, `RPG`]
216
+ },
217
+ {
218
+ type: `member`,
219
+ name: `Create RPG Module (CRTRPGMOD)`,
220
+ command: `CRTRPGMOD MODULE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
221
+ environment: `ile`,
222
+ extensions: [`RPGLE`, `RPG`]
223
+ },
224
+ {
225
+ type: `member`,
226
+ name: `Create SQL ILE RPG Program (CRTSQLRPGI)`,
227
+ command: `CRTSQLRPGI OBJ(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) CLOSQLCSR(*ENDMOD) OPTION(*EVENTF) DBGVIEW(*SOURCE) CVTCCSID(*JOB)`,
228
+ environment: `ile`,
229
+ extensions: [`SQLRPGLE`]
230
+ },
231
+ {
232
+ type: `member`,
233
+ name: `Create SQL ILE RPG Module (CRTSQLRPGI)`,
234
+ command: `CRTSQLRPGI OBJ(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OBJTYPE(*MODULE) CLOSQLCSR(*ENDMOD) OPTION(*EVENTF) DBGVIEW(*SOURCE) CVTCCSID(*JOB)`,
235
+ environment: `ile`,
236
+ extensions: [`SQLRPGLE`]
237
+ },
238
+ {
239
+ type: `member`,
240
+ name: `Create Bound COBOL Program (CRTBNDCBL)`,
241
+ command: `CRTBNDCBL PGM(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
242
+ environment: `ile`,
243
+ extensions: [`CBLLE`]
244
+ },
245
+ {
246
+ type: `member`,
247
+ name: `Create SQL ILE COBOL Program (CRTSQLCBLI)`,
248
+ command: `CRTSQLCBLI OBJ(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) CLOSQLCSR(*ENDMOD) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
249
+ environment: `ile`,
250
+ extensions: [`SQLCBLLE`]
251
+ },
252
+ {
253
+ type: `member`,
254
+ name: `Create Bound C Program (CRTBNDC)`,
255
+ command: `CRTBNDC PGM(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
256
+ environment: `ile`,
257
+ extensions: [`C`]
258
+ },
259
+ {
260
+ type: `member`,
261
+ name: `Create Bound CL Program (CRTBNDCL)`,
262
+ command: `CRTBNDCL PGM(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
263
+ environment: `ile`,
264
+ extensions: [`CLLE`]
265
+ },
266
+ {
267
+ type: `member`,
268
+ name: `Create CL Module (CRTCLMOD)`,
269
+ command: `CRTCLMOD MODULE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF) DBGVIEW(*SOURCE)`,
270
+ environment: `ile`,
271
+ extensions: [`CLLE`]
272
+ },
273
+ {
274
+ type: `member`,
275
+ name: `Create Command (CRTCMD)`,
276
+ command: `CRTCMD CMD(&OPENLIB/&OPENMBR) PGM(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF)`,
277
+ environment: `ile`,
278
+ extensions: [`CMD`]
279
+ },
280
+ {
281
+ type: `member`,
282
+ name: `Create Physical File (CRTPF)`,
283
+ command: `CRTPF FILE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF)`,
284
+ environment: `ile`,
285
+ extensions: [`PF`]
286
+ },
287
+ {
288
+ type: `member`,
289
+ name: `Create Logical File (CRTLF)`,
290
+ command: `CRTLF FILE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF)`,
291
+ environment: `ile`,
292
+ extensions: [`LF`]
293
+ },
294
+ {
295
+ type: `member`,
296
+ name: `Create Printer File (CRTPRTF)`,
297
+ command: `CRTPRTF FILE(&OPENLIB/&OPENMBR) SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) OPTION(*EVENTF)`,
298
+ environment: `ile`,
299
+ extensions: [`PRTF`]
300
+ },
301
+ {
302
+ type: `member`,
303
+ name: `Run SQL Statements from member (RUNSQLSTM)`,
304
+ command: `RUNSQLSTM SRCFILE(&OPENLIB/&OPENSPF) SRCMBR(&OPENMBR) COMMIT(*NONE) NAMING(*SQL)`,
305
+ environment: `ile`,
306
+ extensions: [`SQL`]
307
+ }
182
308
  ]
183
309
  };
@@ -14,6 +14,12 @@ export var Search;
14
14
  };
15
15
  }
16
16
  async function searchMembers(connection, library, sourceFile, searchTerm, members, readOnly) {
17
+ return searchMembersTerms(connection, library, sourceFile, [searchTerm], members, readOnly);
18
+ }
19
+ Search.searchMembers = searchMembers;
20
+ Search.MEMBER_SEARCH_DEFAULT_MAX_MATCHES = 100;
21
+ Search.MEMBER_SEARCH_MAX_MATCHES = 1_000;
22
+ async function searchMembersTerms(connection, library, sourceFile, searchTerms, members, readOnly, options) {
17
23
  const lib = String(library || "").trim().toUpperCase();
18
24
  const src = String(sourceFile || "").trim().toUpperCase();
19
25
  if (!connection.validQsysName(lib)) {
@@ -22,6 +28,11 @@ export var Search;
22
28
  if (!connection.validQsysName(src)) {
23
29
  throw new Error(`Invalid source file: ${sourceFile}`);
24
30
  }
31
+ const terms = searchTerms.map(term => String(term || "")).filter(term => term.length > 0);
32
+ if (terms.length < 1 || terms.length > 20)
33
+ throw new Error("Provide between 1 and 20 search terms");
34
+ if (terms.some(term => term.includes("\n") || term.includes("\r")))
35
+ throw new Error("Search terms may not contain newlines");
25
36
  let detailedMembers;
26
37
  let memberFilter;
27
38
  const pfgrep = connection.remoteFeatures.pfgrep;
@@ -29,42 +40,127 @@ export var Search;
29
40
  if (!isSafeMemberToken(members)) {
30
41
  throw new Error(`Invalid members filter: ${members}`);
31
42
  }
32
- memberFilter = `${members}.MBR`;
43
+ memberFilter = memberGlobToken(members);
33
44
  }
34
45
  else if (Array.isArray(members)) {
46
+ const normalizedMembers = members.map(member => typeof member === "string"
47
+ ? { library: lib, file: src, name: member, extension: "" }
48
+ : member);
35
49
  if (members.length > 1000) {
36
- detailedMembers = members;
50
+ detailedMembers = normalizedMembers;
37
51
  memberFilter = "*.MBR";
38
52
  }
39
53
  else {
40
- members.forEach(member => {
54
+ normalizedMembers.forEach(member => {
41
55
  if (!isSafeMemberToken(member.name)) {
42
56
  throw new Error(`Invalid member name in filter: ${member.name}`);
43
57
  }
44
58
  });
45
- memberFilter = members.map(member => `${member.name}.MBR`).join(` `);
59
+ memberFilter = normalizedMembers.map(member => memberGlobToken(member.name)).join(` `);
46
60
  }
47
61
  }
48
- const command = pfgrep
49
- ? `${pfgrep} -inHr -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`
50
- : `/usr/bin/grep -inHR -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`;
62
+ // pfgrep only supports after-context (-A); before-context needs grep.
63
+ const requestedBefore = clampCount(options?.before, 0, 50, 0);
64
+ const before = pfgrep ? 0 : requestedBefore;
65
+ const after = clampCount(options?.after, 0, 50, 0);
66
+ const maxMatches = clampCount(options?.maxMatches, 1, Search.MEMBER_SEARCH_MAX_MATCHES, Search.MEMBER_SEARCH_DEFAULT_MAX_MATCHES);
67
+ const contextFlags = [
68
+ before ? `-B ${before}` : "",
69
+ after ? `-A ${after}` : ""
70
+ ].filter(Boolean).join(" ");
71
+ // pfgrep documents -F (literal), -f - (patterns from stdin, one per line),
72
+ // -m (matches per member), -A (after-context; it has no -B), -n, -H, -i and -r, all with
73
+ // grep semantics. Patterns go over stdin so terms never reach the shell.
74
+ // -m stops each member early; one extra match per member reveals whether
75
+ // the cap cut anything off. The total across members is bounded by head,
76
+ // which ends the grep as soon as enough lines have been produced.
77
+ const flags = `-F -m ${maxMatches + 1} -f -`;
78
+ const modeFlags = options?.caseSensitive ? "-nHr" : "-inHr";
79
+ const outputLineLimit = (maxMatches + 1) * (before + after + 2);
80
+ const grep = connection.remoteFeatures.grep || "/usr/bin/grep";
81
+ const searchCommand = pfgrep
82
+ ? `${pfgrep} ${modeFlags} ${contextFlags} ${flags} ${memberFilter}`
83
+ : `${grep} ${options?.caseSensitive ? "-nHR" : "-inHR"} ${contextFlags} ${flags} ${memberFilter}`;
84
+ const command = `${searchCommand.replace(/\s+/g, " ")} | /QOpenSys/usr/bin/head -n ${outputLineLimit}`;
51
85
  const result = await connection.sendCommand({
52
86
  command,
53
- directory: `/QSYS.LIB/${lib}.LIB/${src}.FILE`
87
+ directory: `/QSYS.LIB/${lib}.LIB/${src}.FILE`,
88
+ stdin: terms.join("\n")
54
89
  });
55
- if (!result.stderr) {
56
- let hits = parseGrepOutput(result.stdout || '', readOnly);
57
- if (detailedMembers) {
58
- hits = hits.filter(hit => {
59
- const hitMember = parseHitPath(hit);
60
- return detailedMembers.some(member => member.name === hitMember.name && member.library === hitMember.library && member.file === hitMember.file);
61
- });
90
+ const stdout = result.stdout || "";
91
+ if (result.stderr && !stdout.trim()) {
92
+ throw new Error(result.stderr || "Search failed");
93
+ }
94
+ const outputLines = stdout.split("\n").filter(line => line.length > 0);
95
+ // pfgrep prints after-context lines with ":" like matches, so tell them
96
+ // apart by whether the line contains one of the (literal) terms.
97
+ const isMatch = pfgrep && after > 0
98
+ ? (content) => terms.some(term => options?.caseSensitive ? content.includes(term) : content.toLowerCase().includes(term.toLowerCase()))
99
+ : undefined;
100
+ const parsed = parseMemberGrepOutput(stdout, maxMatches, readOnly, isMatch);
101
+ let hits = parsed.hits;
102
+ if (detailedMembers) {
103
+ hits = hits.filter(hit => {
104
+ const hitMember = parseHitPath(hit);
105
+ return detailedMembers.some(member => member.name === hitMember.name && member.library === hitMember.library && member.file === hitMember.file);
106
+ });
107
+ }
108
+ const warnings = [];
109
+ if (pfgrep && requestedBefore > 0)
110
+ warnings.push("pfgrep does not support before-context lines, so only after-context lines are returned.");
111
+ if (result.stderr)
112
+ warnings.push(String(result.stderr).trim());
113
+ const outputLimited = outputLines.length >= outputLineLimit;
114
+ const truncated = parsed.truncated || outputLimited;
115
+ return {
116
+ term: terms[0],
117
+ terms,
118
+ hits,
119
+ matchCount: parsed.matchCount,
120
+ maxMatches,
121
+ truncated,
122
+ ...(truncated ? { truncatedReason: parsed.truncated ? "max-matches" : "output-limit" } : {}),
123
+ ...(warnings.length ? { warnings } : {})
124
+ };
125
+ }
126
+ Search.searchMembersTerms = searchMembersTerms;
127
+ // Parses grep-style output where matching lines use "path:line:text" and
128
+ // context lines use "path-line-text". Keeps at most maxMatches matches and
129
+ // the context that belongs to them.
130
+ function parseMemberGrepOutput(output, maxMatches, readonly, isMatch) {
131
+ const hits = [];
132
+ let matchCount = 0;
133
+ let truncated = false;
134
+ for (const line of output.split("\n")) {
135
+ if (!line || line === "--" || line.startsWith("Binary"))
136
+ continue;
137
+ const parsed = /^(.*?)([:-])(\d+)\2(.*)$/.exec(line);
138
+ if (!parsed)
139
+ continue;
140
+ const kind = parsed[2] === ":" && (!isMatch || isMatch(parsed[4])) ? "match" : "context";
141
+ if (kind === "match") {
142
+ if (matchCount >= maxMatches) {
143
+ truncated = true;
144
+ break;
145
+ }
146
+ matchCount++;
62
147
  }
63
- return { term: searchTerm, hits };
148
+ const path = parsed[1];
149
+ let hit = hits.find(entry => entry.path === path);
150
+ if (!hit) {
151
+ hit = { path, lines: [], readonly };
152
+ hits.push(hit);
153
+ }
154
+ hit.lines.push({ number: Number(parsed[3]), content: parsed[4], kind });
64
155
  }
65
- throw new Error(result.stderr || "Search failed");
156
+ return { hits, matchCount, truncated };
157
+ }
158
+ function clampCount(value, minimum, maximum, fallback) {
159
+ const parsed = Number(value);
160
+ if (value === undefined || value === null || !Number.isFinite(parsed))
161
+ return fallback;
162
+ return Math.max(minimum, Math.min(maximum, Math.floor(parsed)));
66
163
  }
67
- Search.searchMembers = searchMembers;
68
164
  async function searchIFS(connection, path, searchTerm) {
69
165
  const grep = connection.remoteFeatures.grep;
70
166
  if (!grep)
@@ -73,13 +169,10 @@ export var Search;
73
169
  command: `${grep} -inr -F -f - ${Tools.escapePath(path)}`,
74
170
  stdin: searchTerm
75
171
  });
76
- if (grepRes.code === 0) {
77
- return { term: searchTerm, hits: parseGrepOutput(grepRes.stdout) };
78
- }
79
- // grep returns exit code 1 when no matches are found
80
- if (grepRes.code === 1 && !grepRes.stderr) {
81
- return { term: searchTerm, hits: [] };
82
- }
172
+ // grep exits 1 when nothing matched and 2 when any file or directory could
173
+ // not be read, even if other files matched. Keep whatever was printed and
174
+ // report the problems as warnings instead of discarding every hit.
175
+ return withIfsWarnings({ term: searchTerm, hits: parseGrepOutput(grepRes.stdout || ""), exitCode: grepRes.code ?? 0 }, grepRes.stderr);
83
176
  }
84
177
  Search.searchIFS = searchIFS;
85
178
  async function findIFS(connection, path, findTerm) {
@@ -89,55 +182,68 @@ export var Search;
89
182
  const findRes = await connection.sendCommand({
90
183
  command: `${find} ${Tools.escapePath(path)} -type f -iname ${Tools.shellQuote(`*${findTerm}*`)} -print`
91
184
  });
92
- if (findRes.code === 0) {
93
- if (findRes.stdout) {
94
- return { term: findTerm, hits: parseFindOutput(findRes.stdout) };
185
+ // find exits 1 when a single directory is unreadable; the paths it did
186
+ // print are still valid results.
187
+ return withIfsWarnings({ term: findTerm, hits: parseFindOutput(findRes.stdout || ""), exitCode: findRes.code ?? 0 }, findRes.stderr);
188
+ }
189
+ Search.findIFS = findIFS;
190
+ const MAX_IFS_WARNINGS = 50;
191
+ function withIfsWarnings(results, stderr) {
192
+ const lines = String(stderr || "")
193
+ .split(/\r?\n/)
194
+ .map(line => line.trim())
195
+ .filter(line => line.length > 0);
196
+ if (lines.length > 0) {
197
+ const warnings = lines.slice(0, MAX_IFS_WARNINGS);
198
+ if (lines.length > MAX_IFS_WARNINGS) {
199
+ warnings.push(`${lines.length - MAX_IFS_WARNINGS} more warning line(s) omitted`);
95
200
  }
96
- return { term: findTerm, hits: [] };
201
+ results.warnings = warnings;
97
202
  }
203
+ return results;
98
204
  }
99
- Search.findIFS = findIFS;
100
205
  function parseFindOutput(output, readonly, pathTransformer) {
101
206
  const results = [];
102
- for (const line of output.split('\n')) {
207
+ const seen = new Set();
208
+ for (const rawLine of output.split('\n')) {
209
+ const line = rawLine.replace(/\r$/, "");
210
+ if (!line.trim())
211
+ continue;
103
212
  const path = pathTransformer?.(line) || line;
104
- results.push(results.find(r => r.path === path) || { path, readonly, lines: [] });
213
+ if (seen.has(path))
214
+ continue;
215
+ seen.add(path);
216
+ results.push({ path, readonly, lines: [] });
105
217
  }
106
218
  return results;
107
219
  }
108
220
  function parseGrepOutput(output, readonly, pathTransformer) {
109
221
  const results = [];
110
222
  for (const line of output.split('\n')) {
111
- if (line && !line.startsWith(`Binary`)) {
112
- const parts = line.split(`:`);
113
- const path = pathTransformer?.(parts[0]) || parts[0];
223
+ if (line && line !== "--" && !line.startsWith(`Binary`)) {
224
+ const parsed = /^(.*?)([:-])(\d+)\2(.*)$/.exec(line);
225
+ if (!parsed)
226
+ continue;
227
+ const path = pathTransformer?.(parsed[1]) || parsed[1];
114
228
  let result = results.find(r => r.path === path);
115
229
  if (!result) {
116
230
  result = { path, lines: [], readonly };
117
231
  results.push(result);
118
232
  }
119
- const contentIndex = nthIndex(line, `:`, 2);
120
- if (contentIndex >= 0) {
121
- const curContent = line.substring(contentIndex + 1);
122
- result.lines.push({ number: Number(parts[1]), content: curContent });
123
- }
233
+ result.lines.push({ number: Number(parsed[3]), content: parsed[4] });
124
234
  }
125
235
  }
126
236
  return results;
127
237
  }
128
- function sanitizeSearchTerm(searchTerm) {
129
- return searchTerm.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`);
130
- }
131
238
  function isSafeMemberToken(name) {
132
- return /^[A-Za-z0-9_*#]+$/.test(name);
239
+ // IBM i names may contain $, # and @ (and . or _ after the first
240
+ // character); '*' is kept for generic filters such as ABC*.
241
+ return /^[A-Za-z0-9_.*#@$]{1,11}$/.test(name);
133
242
  }
134
- function nthIndex(aString, pattern, n) {
135
- let index = -1;
136
- while (n-- && index++ < aString.length) {
137
- index = aString.indexOf(pattern, index);
138
- if (index < 0)
139
- break;
140
- }
141
- return index;
243
+ // Builds the shell word for a member glob. A leading '#' would start a shell
244
+ // comment and '$' would expand a variable, so both are escaped; '*' stays
245
+ // unescaped so generic names still expand.
246
+ function memberGlobToken(name) {
247
+ return `${name.replace(/[#$]/g, c => `\\${c}`)}.MBR`;
142
248
  }
143
249
  })(Search || (Search = {}));
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { rewriteSqlForQzdfmdb2 } from "./sqlParse.js";
2
3
  export var Tools;
3
4
  (function (Tools) {
4
5
  class SqlError extends Error {
@@ -185,17 +186,13 @@ export var Tools;
185
186
  return { library: parts[0], name: parts[1] };
186
187
  }
187
188
  Tools.parseQSysPath = parseQSysPath;
189
+ /**
190
+ * Prepares SQL for QZDFMDB2: lines starting with `@` become QCMDEXC calls and,
191
+ * with removeComments, `--` comments are dropped. Quote-aware: `--` inside a
192
+ * string literal, delimited identifier or block comment is kept.
193
+ */
188
194
  function fixSQL(statement, removeComments = false) {
189
- let statements = statement.split("\n").map(line => {
190
- if (line.startsWith('@')) {
191
- line = `Call QSYS2.QCMDEXC('${line.substring(1, line.endsWith(";") ? line.length - 1 : undefined).replaceAll("'", "''")}');`;
192
- }
193
- return line.replaceAll("--", "\n--");
194
- }).join(`\n`);
195
- if (removeComments) {
196
- statements = statements.split(`\n`).filter(l => !l.trim().startsWith(`--`)).join(`\n`);
197
- }
198
- return statements;
195
+ return rewriteSqlForQzdfmdb2(statement, removeComments);
199
196
  }
200
197
  Tools.fixSQL = fixSQL;
201
198
  function upperCaseName(value) {