smart-terminal-mcp 1.2.11 → 1.2.12
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/package.json +1 -1
- package/src/command-runner.js +42 -0
- package/src/session-manager.js +29 -2
- package/src/shell-detector.js +1 -1
- package/test/session-manager.test.js +25 -0
package/package.json
CHANGED
package/src/command-runner.js
CHANGED
|
@@ -116,6 +116,9 @@ export async function runCommand({
|
|
|
116
116
|
|
|
117
117
|
if (signal) result.signal = signal;
|
|
118
118
|
if (maxOutputExceeded) result.maxOutputExceeded = true;
|
|
119
|
+
if (successExitCode === null && exitCode !== 0 && exitCode !== null) {
|
|
120
|
+
result.exitCodeIgnored = true;
|
|
121
|
+
}
|
|
119
122
|
if (shouldIncludeSuccessChecks({ successExitCode, successFile })) {
|
|
120
123
|
result.checks = checks.details;
|
|
121
124
|
}
|
|
@@ -229,6 +232,14 @@ function buildSpawnPlan({ cmd, args, cwd }) {
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
const resolvedCommand = resolveWindowsCommand(cmd, cwd);
|
|
235
|
+
|
|
236
|
+
// When the caller explicitly invokes cmd.exe with /c (e.g. for shell
|
|
237
|
+
// built-ins like `for /f`), join the trailing args into a single verbatim
|
|
238
|
+
// command string so cmd.exe interprets them correctly.
|
|
239
|
+
if (isExplicitCmdExeCall(resolvedCommand ?? cmd, args)) {
|
|
240
|
+
return buildCmdExePlan(args);
|
|
241
|
+
}
|
|
242
|
+
|
|
232
243
|
if (!resolvedCommand || !isWindowsBatchCommand(resolvedCommand)) {
|
|
233
244
|
return {
|
|
234
245
|
command: resolvedCommand ?? cmd,
|
|
@@ -244,6 +255,37 @@ function buildSpawnPlan({ cmd, args, cwd }) {
|
|
|
244
255
|
};
|
|
245
256
|
}
|
|
246
257
|
|
|
258
|
+
function isExplicitCmdExeCall(resolved, args) {
|
|
259
|
+
const comSpec = (process.env.ComSpec || 'cmd.exe').toLowerCase();
|
|
260
|
+
const name = resolved.toLowerCase();
|
|
261
|
+
if (name !== comSpec && name !== 'cmd' && name !== 'cmd.exe') return false;
|
|
262
|
+
return args.some((a) => a.toLowerCase() === '/c');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildCmdExePlan(args) {
|
|
266
|
+
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
267
|
+
|
|
268
|
+
// Collect any flags before /c (e.g. /d, /s) and the command body after /c.
|
|
269
|
+
const prefixFlags = [];
|
|
270
|
+
let commandBody = '';
|
|
271
|
+
let foundSlashC = false;
|
|
272
|
+
for (let i = 0; i < args.length; i++) {
|
|
273
|
+
if (!foundSlashC && args[i].toLowerCase() === '/c') {
|
|
274
|
+
foundSlashC = true;
|
|
275
|
+
// Everything after /c is the shell command – join into one string.
|
|
276
|
+
commandBody = args.slice(i + 1).join(' ');
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
prefixFlags.push(args[i]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
command: comSpec,
|
|
284
|
+
args: [...prefixFlags, '/s', '/c', `"${commandBody}"`],
|
|
285
|
+
windowsVerbatimArguments: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
247
289
|
function resolveWindowsCommand(cmd, cwd) {
|
|
248
290
|
const pathExts = getWindowsPathExtensions();
|
|
249
291
|
if (looksLikePath(cmd)) {
|
package/src/session-manager.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import { resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { platform } from 'node:os';
|
|
4
5
|
import { PtySession } from './pty-session.js';
|
|
5
|
-
import { detectShell } from './shell-detector.js';
|
|
6
|
+
import { detectShell, isAvailable } from './shell-detector.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
8
9
|
const MAX_SESSIONS = 10;
|
|
@@ -41,7 +42,7 @@ export class SessionManager {
|
|
|
41
42
|
|
|
42
43
|
const resolvedCwd = await resolveSessionCwd(cwd);
|
|
43
44
|
const detected = detectShell();
|
|
44
|
-
const resolvedShell = shell
|
|
45
|
+
const resolvedShell = shell ? resolveUserShell(shell) : detected.shell;
|
|
45
46
|
const shellArgs = shell ? [] : detected.args;
|
|
46
47
|
|
|
47
48
|
const id = randomUUID().slice(0, 8);
|
|
@@ -121,6 +122,32 @@ export class SessionManager {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
const WINDOWS_SHELL_EXTENSIONS = ['.exe', '.cmd', '.bat'];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate and resolve a user-provided shell name.
|
|
129
|
+
* On Windows, tries appending common extensions if the bare name isn't found.
|
|
130
|
+
* @param {string} shell
|
|
131
|
+
* @returns {string} The resolved shell name
|
|
132
|
+
*/
|
|
133
|
+
function resolveUserShell(shell) {
|
|
134
|
+
if (isAvailable(shell)) return shell;
|
|
135
|
+
|
|
136
|
+
// On Windows, try appending .exe etc. so "pwsh" resolves to "pwsh.exe"
|
|
137
|
+
if (platform() === 'win32' && !shell.includes('.')) {
|
|
138
|
+
for (const ext of WINDOWS_SHELL_EXTENSIONS) {
|
|
139
|
+
const candidate = `${shell}${ext}`;
|
|
140
|
+
if (isAvailable(candidate)) return candidate;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const detected = detectShell();
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Shell "${shell}" not found. The auto-detected shell is "${detected.shell}". ` +
|
|
147
|
+
'Omit the shell parameter to use auto-detection, or provide the full path to the shell executable.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
124
151
|
export async function resolveSessionCwd(cwd) {
|
|
125
152
|
const resolvedCwd = resolvePath(cwd ?? process.cwd());
|
|
126
153
|
|
package/src/shell-detector.js
CHANGED
|
@@ -6,7 +6,7 @@ import { platform } from 'node:os';
|
|
|
6
6
|
* @param {string} exe
|
|
7
7
|
* @returns {boolean}
|
|
8
8
|
*/
|
|
9
|
-
function isAvailable(exe) {
|
|
9
|
+
export function isAvailable(exe) {
|
|
10
10
|
try {
|
|
11
11
|
const cmd = platform() === 'win32' ? 'where' : 'which';
|
|
12
12
|
execFileSync(cmd, [exe], { stdio: 'ignore', timeout: 5000 });
|
|
@@ -42,6 +42,31 @@ test('SessionManager.create rejects invalid cwd before creating a session', asyn
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test('SessionManager.create rejects unknown shell before creating a session', async () => {
|
|
46
|
+
let constructed = 0;
|
|
47
|
+
class FakeSession {
|
|
48
|
+
constructor() {
|
|
49
|
+
constructed++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const manager = new SessionManager({ SessionClass: FakeSession });
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
() => manager.create({ shell: 'nonexistent-shell-abc123' }),
|
|
58
|
+
(error) => {
|
|
59
|
+
assert.match(error.message, /Shell "nonexistent-shell-abc123" not found/);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
assert.equal(constructed, 0);
|
|
64
|
+
assert.equal(manager.list().length, 0);
|
|
65
|
+
} finally {
|
|
66
|
+
manager.destroyAll();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
45
70
|
test('SessionManager.create rejects file cwd values', async () => {
|
|
46
71
|
const tempDir = await mkdtemp(join(tmpdir(), 'smart-terminal-mcp-'));
|
|
47
72
|
const filePath = join(tempDir, 'not-a-directory.txt');
|