wave-agent-sdk 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +32 -13
- package/dist/managers/permissionManager.js +4 -4
- package/dist/utils/bashParser.d.ts +6 -2
- package/dist/utils/bashParser.d.ts.map +1 -1
- package/dist/utils/bashParser.js +38 -4
- package/package.json +1 -1
- package/src/managers/mcpManager.ts +37 -16
- package/src/managers/permissionManager.ts +4 -4
- package/src/utils/bashParser.ts +48 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAuCV,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO/C,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0CvC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrD,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,aAAa,IAAI,eAAe,EAAE;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI;IASzE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO;IAyBzD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqH7C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2BtD,oBAAoB,IAAI,OAAO,EAAE;IAW3B,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC;YAsDY,uBAAuB;IA8D/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,iBAAiB,IAAI,UAAU,EAAE;IA6BjC;;OAEG;IACH,iBAAiB,IAAI,0BAA0B,EAAE;IAIjD;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IActB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAcjC"}
|
|
@@ -25,27 +25,25 @@ export class McpManager {
|
|
|
25
25
|
// Ensure MCP configuration is loaded
|
|
26
26
|
const config = await this.ensureConfigLoaded();
|
|
27
27
|
if (config && config.mcpServers) {
|
|
28
|
-
// Connect to all configured servers
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Connect to all configured servers in background to avoid blocking agent initialization
|
|
29
|
+
Object.keys(config.mcpServers).forEach((serverName) => {
|
|
30
|
+
logger?.debug(`Connecting to MCP server: ${serverName}`);
|
|
31
|
+
this.connectServer(serverName)
|
|
32
|
+
.then((success) => {
|
|
33
33
|
if (success) {
|
|
34
34
|
logger?.debug(`Successfully connected to MCP server: ${serverName}`);
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
37
|
logger?.warn(`Failed to connect to MCP server: ${serverName}`);
|
|
38
38
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
logger?.error(`
|
|
42
|
-
}
|
|
39
|
+
})
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
logger?.error(`Background connection to MCP server ${serverName} failed:`, error);
|
|
42
|
+
});
|
|
43
43
|
});
|
|
44
|
-
// Wait for all connection attempts to complete
|
|
45
|
-
await Promise.all(connectionPromises);
|
|
46
44
|
}
|
|
47
|
-
logger?.debug("MCP servers initialization
|
|
48
|
-
// Trigger state change callback after initialization
|
|
45
|
+
logger?.debug("MCP servers initialization started in background");
|
|
46
|
+
// Trigger state change callback after starting initialization
|
|
49
47
|
this.callbacks.onServersChange?.(this.getAllServers());
|
|
50
48
|
}
|
|
51
49
|
}
|
|
@@ -173,7 +171,28 @@ export class McpManager {
|
|
|
173
171
|
...(server.config.env || {}),
|
|
174
172
|
},
|
|
175
173
|
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
174
|
+
stderr: "pipe", // Pipe stderr to capture it
|
|
176
175
|
});
|
|
176
|
+
// Handle stderr output
|
|
177
|
+
const stderr = transport.stderr;
|
|
178
|
+
if (stderr) {
|
|
179
|
+
let buffer = "";
|
|
180
|
+
stderr.on("data", (chunk) => {
|
|
181
|
+
buffer += chunk.toString();
|
|
182
|
+
const lines = buffer.split("\n");
|
|
183
|
+
buffer = lines.pop() || "";
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
if (line.trim()) {
|
|
186
|
+
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
stderr.on("end", () => {
|
|
191
|
+
if (buffer.trim()) {
|
|
192
|
+
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
177
196
|
// Create client
|
|
178
197
|
const client = new Client({
|
|
179
198
|
name: "wave-code",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { minimatch } from "minimatch";
|
|
10
10
|
import { RESTRICTED_TOOLS } from "../types/permissions.js";
|
|
11
|
-
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections,
|
|
11
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, isBashHeredocWrite, getSmartPrefix, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
|
|
12
12
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
13
13
|
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, } from "../constants/tools.js";
|
|
14
14
|
const SAFE_COMMANDS = [
|
|
@@ -266,10 +266,10 @@ export class PermissionManager {
|
|
|
266
266
|
* Called by individual tools after validation/diff, before real operation
|
|
267
267
|
*/
|
|
268
268
|
async checkPermission(context) {
|
|
269
|
-
// 0. Intercept Bash
|
|
269
|
+
// 0. Intercept Bash EOF writing operations
|
|
270
270
|
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
271
271
|
const command = String(context.toolInput.command);
|
|
272
|
-
if (
|
|
272
|
+
if (isBashHeredocWrite(command)) {
|
|
273
273
|
// Check if this specific command is explicitly allowed by a rule that includes redirection
|
|
274
274
|
const isExplicitlyAllowed = [
|
|
275
275
|
...this.instanceAllowedRules,
|
|
@@ -289,7 +289,7 @@ export class PermissionManager {
|
|
|
289
289
|
if (!isExplicitlyAllowed) {
|
|
290
290
|
return {
|
|
291
291
|
behavior: "deny",
|
|
292
|
-
message: "Bash-based file writing operations (e.g.,
|
|
292
|
+
message: "Bash-based file writing operations using heredocs (e.g., 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -16,9 +16,13 @@ export declare function stripRedirections(command: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function hasWriteRedirections(command: string): boolean;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function hasHeredoc(command: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isBashHeredocWrite(command: string): boolean;
|
|
22
26
|
/**
|
|
23
27
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
24
28
|
* and should not have persistent permissions.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAoH1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuHzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkG7D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCnD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8L7D"}
|
package/dist/utils/bashParser.js
CHANGED
|
@@ -53,7 +53,7 @@ export function splitBashCommand(command) {
|
|
|
53
53
|
opLen = 1;
|
|
54
54
|
else if (char === "|")
|
|
55
55
|
opLen = 1;
|
|
56
|
-
else if (char === "&" && nextChar !== ">")
|
|
56
|
+
else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
|
|
57
57
|
opLen = 1;
|
|
58
58
|
if (opLen > 0) {
|
|
59
59
|
// Check if preceded by an odd number of backslashes
|
|
@@ -361,10 +361,44 @@ export function hasWriteRedirections(command) {
|
|
|
361
361
|
return false;
|
|
362
362
|
}
|
|
363
363
|
/**
|
|
364
|
-
*
|
|
364
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
365
365
|
*/
|
|
366
|
-
export function
|
|
367
|
-
|
|
366
|
+
export function hasHeredoc(command) {
|
|
367
|
+
let inSingleQuote = false;
|
|
368
|
+
let inDoubleQuote = false;
|
|
369
|
+
let escaped = false;
|
|
370
|
+
for (let i = 0; i < command.length; i++) {
|
|
371
|
+
const char = command[i];
|
|
372
|
+
if (escaped) {
|
|
373
|
+
escaped = false;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (char === "\\") {
|
|
377
|
+
escaped = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (char === "'" && !inDoubleQuote) {
|
|
381
|
+
inSingleQuote = !inSingleQuote;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (char === '"' && !inSingleQuote) {
|
|
385
|
+
inDoubleQuote = !inDoubleQuote;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (char === "<" && command[i + 1] === "<") {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
399
|
+
*/
|
|
400
|
+
export function isBashHeredocWrite(command) {
|
|
401
|
+
return hasHeredoc(command) && hasWriteRedirections(command);
|
|
368
402
|
}
|
|
369
403
|
/**
|
|
370
404
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
package/package.json
CHANGED
|
@@ -63,12 +63,11 @@ export class McpManager {
|
|
|
63
63
|
const config = await this.ensureConfigLoaded();
|
|
64
64
|
|
|
65
65
|
if (config && config.mcpServers) {
|
|
66
|
-
// Connect to all configured servers
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const success = await this.connectServer(serverName);
|
|
66
|
+
// Connect to all configured servers in background to avoid blocking agent initialization
|
|
67
|
+
Object.keys(config.mcpServers).forEach((serverName) => {
|
|
68
|
+
logger?.debug(`Connecting to MCP server: ${serverName}`);
|
|
69
|
+
this.connectServer(serverName)
|
|
70
|
+
.then((success) => {
|
|
72
71
|
if (success) {
|
|
73
72
|
logger?.debug(
|
|
74
73
|
`Successfully connected to MCP server: ${serverName}`,
|
|
@@ -76,18 +75,18 @@ export class McpManager {
|
|
|
76
75
|
} else {
|
|
77
76
|
logger?.warn(`Failed to connect to MCP server: ${serverName}`);
|
|
78
77
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
})
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
logger?.error(
|
|
81
|
+
`Background connection to MCP server ${serverName} failed:`,
|
|
82
|
+
error,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
logger?.debug("MCP servers initialization
|
|
90
|
-
// Trigger state change callback after initialization
|
|
88
|
+
logger?.debug("MCP servers initialization started in background");
|
|
89
|
+
// Trigger state change callback after starting initialization
|
|
91
90
|
this.callbacks.onServersChange?.(this.getAllServers());
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -233,8 +232,30 @@ export class McpManager {
|
|
|
233
232
|
...(server.config.env || {}),
|
|
234
233
|
},
|
|
235
234
|
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
235
|
+
stderr: "pipe", // Pipe stderr to capture it
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
+
// Handle stderr output
|
|
239
|
+
const stderr = transport.stderr;
|
|
240
|
+
if (stderr) {
|
|
241
|
+
let buffer = "";
|
|
242
|
+
stderr.on("data", (chunk: Buffer) => {
|
|
243
|
+
buffer += chunk.toString();
|
|
244
|
+
const lines = buffer.split("\n");
|
|
245
|
+
buffer = lines.pop() || "";
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (line.trim()) {
|
|
248
|
+
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
stderr.on("end", () => {
|
|
253
|
+
if (buffer.trim()) {
|
|
254
|
+
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
238
259
|
// Create client
|
|
239
260
|
const client = new Client(
|
|
240
261
|
{
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
stripEnvVars,
|
|
22
22
|
stripRedirections,
|
|
23
23
|
hasWriteRedirections,
|
|
24
|
-
|
|
24
|
+
isBashHeredocWrite,
|
|
25
25
|
getSmartPrefix,
|
|
26
26
|
DANGEROUS_COMMANDS,
|
|
27
27
|
} from "../utils/bashParser.js";
|
|
@@ -370,10 +370,10 @@ export class PermissionManager {
|
|
|
370
370
|
async checkPermission(
|
|
371
371
|
context: ToolPermissionContext,
|
|
372
372
|
): Promise<PermissionDecision> {
|
|
373
|
-
// 0. Intercept Bash
|
|
373
|
+
// 0. Intercept Bash EOF writing operations
|
|
374
374
|
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
375
375
|
const command = String(context.toolInput.command);
|
|
376
|
-
if (
|
|
376
|
+
if (isBashHeredocWrite(command)) {
|
|
377
377
|
// Check if this specific command is explicitly allowed by a rule that includes redirection
|
|
378
378
|
const isExplicitlyAllowed = [
|
|
379
379
|
...this.instanceAllowedRules,
|
|
@@ -395,7 +395,7 @@ export class PermissionManager {
|
|
|
395
395
|
return {
|
|
396
396
|
behavior: "deny",
|
|
397
397
|
message:
|
|
398
|
-
"Bash-based file writing operations (e.g.,
|
|
398
|
+
"Bash-based file writing operations using heredocs (e.g., 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
|
|
399
399
|
};
|
|
400
400
|
}
|
|
401
401
|
}
|
package/src/utils/bashParser.ts
CHANGED
|
@@ -58,7 +58,8 @@ export function splitBashCommand(command: string): string[] {
|
|
|
58
58
|
else if (char === "|" && nextChar === "&") opLen = 2;
|
|
59
59
|
else if (char === ";") opLen = 1;
|
|
60
60
|
else if (char === "|") opLen = 1;
|
|
61
|
-
else if (char === "&" && nextChar !== ">"
|
|
61
|
+
else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
|
|
62
|
+
opLen = 1;
|
|
62
63
|
|
|
63
64
|
if (opLen > 0) {
|
|
64
65
|
// Check if preceded by an odd number of backslashes
|
|
@@ -395,10 +396,53 @@ export function hasWriteRedirections(command: string): boolean {
|
|
|
395
396
|
}
|
|
396
397
|
|
|
397
398
|
/**
|
|
398
|
-
*
|
|
399
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
399
400
|
*/
|
|
400
|
-
export function
|
|
401
|
-
|
|
401
|
+
export function hasHeredoc(command: string): boolean {
|
|
402
|
+
let inSingleQuote = false;
|
|
403
|
+
let inDoubleQuote = false;
|
|
404
|
+
let escaped = false;
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < command.length; i++) {
|
|
407
|
+
const char = command[i];
|
|
408
|
+
|
|
409
|
+
if (escaped) {
|
|
410
|
+
escaped = false;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (char === "\\") {
|
|
415
|
+
escaped = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (char === "'" && !inDoubleQuote) {
|
|
420
|
+
inSingleQuote = !inSingleQuote;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (char === '"' && !inSingleQuote) {
|
|
425
|
+
inDoubleQuote = !inDoubleQuote;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (char === "<" && command[i + 1] === "<") {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
443
|
+
*/
|
|
444
|
+
export function isBashHeredocWrite(command: string): boolean {
|
|
445
|
+
return hasHeredoc(command) && hasWriteRedirections(command);
|
|
402
446
|
}
|
|
403
447
|
|
|
404
448
|
/**
|