start-command 0.24.0 → 0.24.2
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/CHANGELOG.md +35 -0
- package/package.json +1 -1
- package/src/lib/isolation.js +48 -47
- package/test/isolation.test.js +56 -0
- package/test/regression-84.test.js +159 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f870146: perf: skip shell detection probe when command is a bare shell invocation
|
|
8
|
+
|
|
9
|
+
When running `$ --isolated docker -- bash`, the tool previously ran a probe
|
|
10
|
+
container (`docker run --rm image sh -c 'command -v bash'`) to detect which
|
|
11
|
+
shell to use, even though the result was unused for bare shell commands.
|
|
12
|
+
|
|
13
|
+
Now `isInteractiveShellCommand(command)` is evaluated first, and
|
|
14
|
+
`detectShellInEnvironment` is only called when the command is not a bare shell.
|
|
15
|
+
This avoids up to three unnecessary container starts per invocation and eliminates
|
|
16
|
+
spurious failures when the probe itself fails on images with complex entrypoints.
|
|
17
|
+
|
|
18
|
+
Also caches the `isInteractiveShellCommand(command)` result in `isBareShell` to
|
|
19
|
+
avoid redundant calls in both attached and detached code paths.
|
|
20
|
+
|
|
21
|
+
## 0.24.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 2cb0101: fix: avoid running shell inside shell when command is a bare shell invocation
|
|
26
|
+
|
|
27
|
+
When running `$ --isolated docker --image <image> -- bash`, the tool was
|
|
28
|
+
wrapping the bare `bash` command inside `bash -i -c bash`, causing `.bashrc`
|
|
29
|
+
to be sourced twice and printing errors twice. Same issue affected `zsh`, `sh`,
|
|
30
|
+
and other shells.
|
|
31
|
+
|
|
32
|
+
Added `isInteractiveShellCommand()` helper that detects when the user's command
|
|
33
|
+
is a bare interactive shell invocation (e.g. `bash`, `/bin/zsh`, `bash -l`).
|
|
34
|
+
When detected, the command is passed directly to the isolation environment
|
|
35
|
+
instead of being wrapped in another shell. Applies to Docker (attached and
|
|
36
|
+
detached), SSH (attached and detached), and Screen (attached and detached).
|
|
37
|
+
|
|
3
38
|
## 0.24.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/package.json
CHANGED
package/src/lib/isolation.js
CHANGED
|
@@ -216,21 +216,19 @@ function getShellInteractiveFlag(shellPath) {
|
|
|
216
216
|
const shellName = shellPath.split('/').pop();
|
|
217
217
|
return shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
218
218
|
}
|
|
219
|
+
/** True if command is a bare shell invocation (no -c); avoids bash-inside-bash (issue #84). */
|
|
220
|
+
function isInteractiveShellCommand(command) {
|
|
221
|
+
const parts = command.trim().split(/\s+/);
|
|
222
|
+
const shells = ['bash', 'zsh', 'sh', 'fish', 'ksh', 'csh', 'tcsh', 'dash'];
|
|
223
|
+
return shells.includes(path.basename(parts[0])) && !parts.includes('-c');
|
|
224
|
+
}
|
|
219
225
|
|
|
220
|
-
/**
|
|
221
|
-
* Check if the current process has a TTY attached
|
|
222
|
-
* @returns {boolean} True if TTY is available
|
|
223
|
-
*/
|
|
226
|
+
/** Returns true if the current process has a TTY attached. */
|
|
224
227
|
function hasTTY() {
|
|
225
228
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
/**
|
|
229
|
-
* Wrap command with sudo -u if user option is specified
|
|
230
|
-
* @param {string} command - Original command
|
|
231
|
-
* @param {string|null} user - Username to run as (or null)
|
|
232
|
-
* @returns {string} Wrapped command
|
|
233
|
-
*/
|
|
231
|
+
/** Wraps command with sudo -u if user is specified; returns command unchanged otherwise. */
|
|
234
232
|
function wrapCommandWithUser(command, user) {
|
|
235
233
|
if (!user) {
|
|
236
234
|
return command;
|
|
@@ -242,13 +240,8 @@ function wrapCommandWithUser(command, user) {
|
|
|
242
240
|
}
|
|
243
241
|
|
|
244
242
|
/**
|
|
245
|
-
* Run command in GNU Screen using detached mode with log capture
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* Supports two methods based on screen version:
|
|
249
|
-
* - screen >= 4.5.1: Uses -L -Logfile option for native log capture
|
|
250
|
-
* - screen < 4.5.1: Uses tee command within the wrapped command for output capture
|
|
251
|
-
*
|
|
243
|
+
* Run command in GNU Screen using detached mode with log capture.
|
|
244
|
+
* Supports screen >= 4.5.1 (native -Logfile) and older versions (tee fallback).
|
|
252
245
|
* @param {string} command - Command to execute
|
|
253
246
|
* @param {string} sessionName - Session name
|
|
254
247
|
* @param {object} shellInfo - Shell info from getShell()
|
|
@@ -271,16 +264,10 @@ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
|
|
|
271
264
|
if (useNativeLogging) {
|
|
272
265
|
// Modern screen (>= 4.5.1): Use -L -Logfile option for native log capture
|
|
273
266
|
// screen -dmS <session> -L -Logfile <logfile> <shell> -c '<command>'
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
'-Logfile',
|
|
279
|
-
logFile,
|
|
280
|
-
shell,
|
|
281
|
-
shellArg,
|
|
282
|
-
effectiveCommand,
|
|
283
|
-
];
|
|
267
|
+
const logArgs = ['-dmS', sessionName, '-L', '-Logfile', logFile];
|
|
268
|
+
screenArgs = isInteractiveShellCommand(command)
|
|
269
|
+
? [...logArgs, ...command.trim().split(/\s+/)]
|
|
270
|
+
: [...logArgs, shell, shellArg, effectiveCommand];
|
|
284
271
|
|
|
285
272
|
if (DEBUG) {
|
|
286
273
|
console.log(
|
|
@@ -289,10 +276,14 @@ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
|
|
|
289
276
|
}
|
|
290
277
|
} else {
|
|
291
278
|
// Older screen (< 4.5.1, e.g., macOS bundled 4.0.3): Use tee fallback
|
|
292
|
-
// Wrap the command to capture output using tee
|
|
293
279
|
// The parentheses ensure proper grouping of the command and its stderr
|
|
294
|
-
|
|
295
|
-
|
|
280
|
+
const isBareShell = isInteractiveShellCommand(command);
|
|
281
|
+
if (!isBareShell) {
|
|
282
|
+
effectiveCommand = `(${effectiveCommand}) 2>&1 | tee "${logFile}"`;
|
|
283
|
+
}
|
|
284
|
+
screenArgs = isBareShell
|
|
285
|
+
? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
|
|
286
|
+
: ['-dmS', sessionName, shell, shellArg, effectiveCommand];
|
|
296
287
|
|
|
297
288
|
if (DEBUG) {
|
|
298
289
|
console.log(
|
|
@@ -451,13 +442,9 @@ function runInScreen(command, options = {}) {
|
|
|
451
442
|
}
|
|
452
443
|
// Without keep-alive: command runs and session exits naturally when done
|
|
453
444
|
|
|
454
|
-
const screenArgs =
|
|
455
|
-
'-dmS',
|
|
456
|
-
sessionName,
|
|
457
|
-
shell,
|
|
458
|
-
shellArg,
|
|
459
|
-
effectiveCommand,
|
|
460
|
-
];
|
|
445
|
+
const screenArgs = isInteractiveShellCommand(command)
|
|
446
|
+
? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
|
|
447
|
+
: ['-dmS', sessionName, shell, shellArg, effectiveCommand];
|
|
461
448
|
|
|
462
449
|
if (DEBUG) {
|
|
463
450
|
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
@@ -670,7 +657,9 @@ function runInSsh(command, options = {}) {
|
|
|
670
657
|
const shellInvocation = shellInteractiveFlag
|
|
671
658
|
? `${remoteShell} ${shellInteractiveFlag}`
|
|
672
659
|
: remoteShell;
|
|
673
|
-
const remoteCommand =
|
|
660
|
+
const remoteCommand = isInteractiveShellCommand(command)
|
|
661
|
+
? `nohup ${command} > /tmp/${sessionName}.log 2>&1 &`
|
|
662
|
+
: `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
|
|
674
663
|
const sshArgs = [sshTarget, remoteCommand];
|
|
675
664
|
|
|
676
665
|
if (DEBUG) {
|
|
@@ -692,9 +681,11 @@ function runInSsh(command, options = {}) {
|
|
|
692
681
|
} else {
|
|
693
682
|
// Attached mode: pass command directly (auto) or wrap with explicit shell + -i flag (bash/zsh).
|
|
694
683
|
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
695
|
-
const sshArgs =
|
|
696
|
-
? [sshTarget,
|
|
697
|
-
:
|
|
684
|
+
const sshArgs = isInteractiveShellCommand(command)
|
|
685
|
+
? [sshTarget, ...command.trim().split(/\s+/)]
|
|
686
|
+
: useExplicitShell
|
|
687
|
+
? [sshTarget, useExplicitShell, ...extraFlags, '-c', command]
|
|
688
|
+
: [sshTarget, command];
|
|
698
689
|
|
|
699
690
|
if (DEBUG) {
|
|
700
691
|
console.log(`[DEBUG] Running: ssh ${sshArgs.join(' ')}`);
|
|
@@ -775,7 +766,10 @@ function runInDocker(command, options = {}) {
|
|
|
775
766
|
}
|
|
776
767
|
}
|
|
777
768
|
|
|
778
|
-
const
|
|
769
|
+
const isBareShell = isInteractiveShellCommand(command);
|
|
770
|
+
const shellToUse = isBareShell
|
|
771
|
+
? 'sh'
|
|
772
|
+
: detectShellInEnvironment('docker', options, options.shell);
|
|
779
773
|
const shellInteractiveFlag = getShellInteractiveFlag(shellToUse);
|
|
780
774
|
|
|
781
775
|
// Print the user command (this appears after any virtual commands like docker pull)
|
|
@@ -786,9 +780,6 @@ function runInDocker(command, options = {}) {
|
|
|
786
780
|
try {
|
|
787
781
|
if (options.detached) {
|
|
788
782
|
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
789
|
-
const effectiveCommand = options.keepAlive
|
|
790
|
-
? `${command}; exec ${shellToUse}`
|
|
791
|
-
: command;
|
|
792
783
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
793
784
|
|
|
794
785
|
// --rm must come before the image name
|
|
@@ -801,10 +792,16 @@ function runInDocker(command, options = {}) {
|
|
|
801
792
|
dockerArgs.push('--user', options.user);
|
|
802
793
|
}
|
|
803
794
|
|
|
795
|
+
const effectiveCommand = options.keepAlive
|
|
796
|
+
? `${command}; exec ${shellToUse}`
|
|
797
|
+
: command;
|
|
804
798
|
const shellArgs = shellInteractiveFlag
|
|
805
799
|
? [shellToUse, shellInteractiveFlag]
|
|
806
800
|
: [shellToUse];
|
|
807
|
-
|
|
801
|
+
const cmdArgs = isBareShell
|
|
802
|
+
? command.trim().split(/\s+/)
|
|
803
|
+
: [...shellArgs, '-c', effectiveCommand];
|
|
804
|
+
dockerArgs.push(options.image, ...cmdArgs);
|
|
808
805
|
|
|
809
806
|
if (DEBUG) {
|
|
810
807
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -853,7 +850,10 @@ function runInDocker(command, options = {}) {
|
|
|
853
850
|
const shellCmdArgs = shellInteractiveFlag
|
|
854
851
|
? [shellToUse, shellInteractiveFlag]
|
|
855
852
|
: [shellToUse];
|
|
856
|
-
|
|
853
|
+
const attachedCmdArgs = isBareShell
|
|
854
|
+
? command.trim().split(/\s+/)
|
|
855
|
+
: [...shellCmdArgs, '-c', command];
|
|
856
|
+
dockerArgs.push(options.image, ...attachedCmdArgs);
|
|
857
857
|
|
|
858
858
|
if (DEBUG) {
|
|
859
859
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -972,6 +972,7 @@ const {
|
|
|
972
972
|
module.exports = {
|
|
973
973
|
isCommandAvailable,
|
|
974
974
|
hasTTY,
|
|
975
|
+
isInteractiveShellCommand,
|
|
975
976
|
detectShellInEnvironment,
|
|
976
977
|
runInScreen,
|
|
977
978
|
runInTmux,
|
package/test/isolation.test.js
CHANGED
|
@@ -896,3 +896,59 @@ describe('Shell option forwarding to isolation runners', () => {
|
|
|
896
896
|
});
|
|
897
897
|
});
|
|
898
898
|
});
|
|
899
|
+
|
|
900
|
+
describe('isInteractiveShellCommand (issue #84)', () => {
|
|
901
|
+
const { isInteractiveShellCommand } = require('../src/lib/isolation');
|
|
902
|
+
|
|
903
|
+
it('should return true for "bash"', () => {
|
|
904
|
+
assert.strictEqual(isInteractiveShellCommand('bash'), true);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should return true for "zsh"', () => {
|
|
908
|
+
assert.strictEqual(isInteractiveShellCommand('zsh'), true);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should return true for "sh"', () => {
|
|
912
|
+
assert.strictEqual(isInteractiveShellCommand('sh'), true);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('should return true for "/bin/bash"', () => {
|
|
916
|
+
assert.strictEqual(isInteractiveShellCommand('/bin/bash'), true);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should return true for "/usr/bin/zsh"', () => {
|
|
920
|
+
assert.strictEqual(isInteractiveShellCommand('/usr/bin/zsh'), true);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('should return true for "bash -l" (login flag, no -c)', () => {
|
|
924
|
+
assert.strictEqual(isInteractiveShellCommand('bash -l'), true);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('should return false for "bash -c echo"', () => {
|
|
928
|
+
assert.strictEqual(isInteractiveShellCommand('bash -c echo'), false);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should return false for "bash -c \'echo hi\'"', () => {
|
|
932
|
+
assert.strictEqual(isInteractiveShellCommand("bash -c 'echo hi'"), false);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('should return false for "npm test"', () => {
|
|
936
|
+
assert.strictEqual(isInteractiveShellCommand('npm test'), false);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('should return false for "echo hello"', () => {
|
|
940
|
+
assert.strictEqual(isInteractiveShellCommand('echo hello'), false);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should return false for "ls -la"', () => {
|
|
944
|
+
assert.strictEqual(isInteractiveShellCommand('ls -la'), false);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should return false for empty string', () => {
|
|
948
|
+
assert.strictEqual(isInteractiveShellCommand(''), false);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('should return false for " "', () => {
|
|
952
|
+
assert.strictEqual(isInteractiveShellCommand(' '), false);
|
|
953
|
+
});
|
|
954
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Regression tests for issue #84: "We should not run bash inside bash"
|
|
4
|
+
*
|
|
5
|
+
* These tests guard against the shell-inside-shell regression where
|
|
6
|
+
* `$ --isolated docker -- bash` caused:
|
|
7
|
+
* docker run ... image /bin/bash -i -c bash (WRONG: bash inside bash)
|
|
8
|
+
* instead of:
|
|
9
|
+
* docker run ... image bash (CORRECT: bare shell)
|
|
10
|
+
*
|
|
11
|
+
* The same regression applies to zsh, sh, and all other isolation backends.
|
|
12
|
+
*
|
|
13
|
+
* Reference: https://github.com/link-foundation/start/issues/84
|
|
14
|
+
* Fixed in: PR #85 (v0.24.1) via isInteractiveShellCommand()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { describe, it } = require('node:test');
|
|
18
|
+
const assert = require('assert');
|
|
19
|
+
const { isInteractiveShellCommand } = require('../src/lib/isolation');
|
|
20
|
+
|
|
21
|
+
// Helper: mirrors the command-args construction logic used in
|
|
22
|
+
// runInDocker (attached + detached), runInScreen, and runInSsh.
|
|
23
|
+
// If this helper returns args containing '-c' for a bare shell command,
|
|
24
|
+
// the shell-inside-shell bug is present.
|
|
25
|
+
function buildCmdArgs(command, shellToUse = '/bin/bash') {
|
|
26
|
+
const shellName = shellToUse.split('/').pop();
|
|
27
|
+
const shellInteractiveFlag =
|
|
28
|
+
shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
29
|
+
const shellArgs = shellInteractiveFlag
|
|
30
|
+
? [shellToUse, shellInteractiveFlag]
|
|
31
|
+
: [shellToUse];
|
|
32
|
+
return isInteractiveShellCommand(command)
|
|
33
|
+
? command.trim().split(/\s+/)
|
|
34
|
+
: [...shellArgs, '-c', command];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('isInteractiveShellCommand additional cases (issue #84)', () => {
|
|
38
|
+
// These cover edge cases not in the base isInteractiveShellCommand test suite.
|
|
39
|
+
|
|
40
|
+
// Workaround: bash --norc skips .bashrc sourcing (post-fix regression workaround)
|
|
41
|
+
it('should return true for "bash --norc"', () => {
|
|
42
|
+
assert.strictEqual(isInteractiveShellCommand('bash --norc'), true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return true for "zsh --no-rcs"', () => {
|
|
46
|
+
assert.strictEqual(isInteractiveShellCommand('zsh --no-rcs'), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return true for "bash -i" (interactive flag, no -c)', () => {
|
|
50
|
+
assert.strictEqual(isInteractiveShellCommand('bash -i'), true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return true for "fish"', () => {
|
|
54
|
+
assert.strictEqual(isInteractiveShellCommand('fish'), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return true for "dash"', () => {
|
|
58
|
+
assert.strictEqual(isInteractiveShellCommand('dash'), true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return true for "/usr/local/bin/bash"', () => {
|
|
62
|
+
assert.strictEqual(isInteractiveShellCommand('/usr/local/bin/bash'), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return false for \'bash -c "echo hello"\'', () => {
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
isInteractiveShellCommand('bash -c "echo hello"'),
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
74
|
+
// Each test verifies that the command-arg construction logic does NOT
|
|
75
|
+
// wrap a bare shell invocation inside another shell with `-c`.
|
|
76
|
+
//
|
|
77
|
+
// Before fix: buildCmdArgs('bash') → ['/bin/bash', '-i', '-c', 'bash']
|
|
78
|
+
// After fix: buildCmdArgs('bash') → ['bash']
|
|
79
|
+
|
|
80
|
+
it('should pass "bash" directly, not wrap in shell -c', () => {
|
|
81
|
+
const args = buildCmdArgs('bash');
|
|
82
|
+
assert.deepStrictEqual(
|
|
83
|
+
args,
|
|
84
|
+
['bash'],
|
|
85
|
+
`Expected ["bash"], got: ${JSON.stringify(args)}`
|
|
86
|
+
);
|
|
87
|
+
assert.ok(
|
|
88
|
+
!args.includes('-c'),
|
|
89
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should pass "zsh" directly, not wrap in shell -c', () => {
|
|
94
|
+
const args = buildCmdArgs('zsh');
|
|
95
|
+
assert.deepStrictEqual(args, ['zsh']);
|
|
96
|
+
assert.ok(
|
|
97
|
+
!args.includes('-c'),
|
|
98
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should pass "sh" directly, not wrap in shell -c', () => {
|
|
103
|
+
const args = buildCmdArgs('sh', 'sh');
|
|
104
|
+
assert.deepStrictEqual(args, ['sh']);
|
|
105
|
+
assert.ok(
|
|
106
|
+
!args.includes('-c'),
|
|
107
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should pass "/bin/bash" directly, not wrap in shell -c', () => {
|
|
112
|
+
const args = buildCmdArgs('/bin/bash');
|
|
113
|
+
assert.deepStrictEqual(args, ['/bin/bash']);
|
|
114
|
+
assert.ok(
|
|
115
|
+
!args.includes('-c'),
|
|
116
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should pass "bash --norc" directly (workaround for broken .bashrc)', () => {
|
|
121
|
+
const args = buildCmdArgs('bash --norc');
|
|
122
|
+
assert.deepStrictEqual(args, ['bash', '--norc']);
|
|
123
|
+
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should pass "bash -l" directly (login shell)', () => {
|
|
127
|
+
const args = buildCmdArgs('bash -l');
|
|
128
|
+
assert.deepStrictEqual(args, ['bash', '-l']);
|
|
129
|
+
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should still wrap non-shell commands in shell -c (guard against over-broad fix)', () => {
|
|
133
|
+
const args = buildCmdArgs('echo hello', '/bin/bash');
|
|
134
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'echo hello']);
|
|
135
|
+
assert.ok(
|
|
136
|
+
args.includes('-c'),
|
|
137
|
+
'Non-shell commands must still use -c wrapper'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should still wrap "npm test" in shell -c (guard against over-broad fix)', () => {
|
|
142
|
+
const args = buildCmdArgs('npm test', '/bin/bash');
|
|
143
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'npm test']);
|
|
144
|
+
assert.ok(
|
|
145
|
+
args.includes('-c'),
|
|
146
|
+
'Non-shell commands must still use -c wrapper'
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not treat "bash -c something" as bare shell', () => {
|
|
151
|
+
// bash -c ... has -c, so isInteractiveShellCommand returns false
|
|
152
|
+
// The command gets wrapped: ['/bin/bash', '-i', '-c', 'bash -c "echo hi"']
|
|
153
|
+
const args = buildCmdArgs('bash -c "echo hi"', '/bin/bash');
|
|
154
|
+
assert.ok(
|
|
155
|
+
args.includes('-c'),
|
|
156
|
+
'bash -c commands should be treated as regular commands'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|